mirror of
				https://github.com/vcmi/vcmi.git
				synced 2025-10-31 00:07:39 +02:00 
			
		
		
		
	Merge branch 'vcmi/beta' into 'vcmi/develop'
This commit is contained in:
		
							
								
								
									
										51
									
								
								.github/workflows/github.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										51
									
								
								.github/workflows/github.yml
									
									
									
									
										vendored
									
									
								
							| @@ -88,16 +88,16 @@ jobs: | ||||
|             preset: windows-mingw-conan-linux | ||||
|             conan_profile: mingw32-linux.jinja | ||||
|           - platform: android-32 | ||||
|             os: ubuntu-22.04 | ||||
|             os: macos-14 | ||||
|             extension: apk | ||||
|             preset: android-conan-ninja-release | ||||
|             preset: android-daily-release | ||||
|             conan_profile: android-32 | ||||
|             conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT | ||||
|             artifact_platform: armeabi-v7a | ||||
|           - platform: android-64 | ||||
|             os: ubuntu-22.04 | ||||
|             os: macos-14 | ||||
|             extension: apk | ||||
|             preset: android-conan-ninja-release | ||||
|             preset: android-daily-release | ||||
|             conan_profile: android-64 | ||||
|             conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT | ||||
|             artifact_platform: arm64-v8a | ||||
| @@ -187,6 +187,12 @@ jobs: | ||||
|       env: | ||||
|         GENERATE_ONLY_BUILT_CONFIG: 1 | ||||
|  | ||||
|     - uses: actions/setup-java@v4 | ||||
|       if: ${{ startsWith(matrix.platform, 'android') }} | ||||
|       with: | ||||
|         distribution: 'temurin' | ||||
|         java-version: '11' | ||||
|  | ||||
|     - name: Build Number | ||||
|       run: | | ||||
|         source '${{github.workspace}}/CI/get_package_name.sh' | ||||
| @@ -215,7 +221,7 @@ jobs: | ||||
|       run: | | ||||
|         ctest --preset ${{matrix.preset}} | ||||
|  | ||||
|     - name: Kill XProtect to work around CPack issue on macOS  | ||||
|     - name: Kill XProtect to work around CPack issue on macOS | ||||
|       if: ${{ startsWith(matrix.platform, 'mac') }} | ||||
|       run: | | ||||
|         # Cf. https://github.com/actions/runner-images/issues/7522#issuecomment-1556766641 | ||||
| @@ -234,13 +240,6 @@ jobs: | ||||
|           && '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)" | ||||
|         rm -rf _CPack_Packages | ||||
|  | ||||
|     - name: Create Android package | ||||
|       if: ${{ startsWith(matrix.platform, 'android') }} | ||||
|       run: | | ||||
|         cd android | ||||
|         ./gradlew assembleDaily --info | ||||
|         echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/android/vcmi-app/build/outputs/apk/daily/*.${{ matrix.extension }})" >> $GITHUB_ENV | ||||
|  | ||||
|     - name: Additional logs | ||||
|       if: ${{ failure() && steps.cpack.outcome == 'failure' && matrix.platform == 'msvc' }} | ||||
|       run: | | ||||
| @@ -254,7 +253,14 @@ jobs: | ||||
|         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} | ||||
|         path: | | ||||
|           ${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }} | ||||
|            | ||||
|  | ||||
|     - name: Find Android package | ||||
|       if: ${{ startsWith(matrix.platform, 'android') }} | ||||
|       run: | | ||||
|         builtApkPath="$(ls ${{ github.workspace }}/out/build/${{ matrix.preset }}/android-build/vcmi-app/build/outputs/apk/release/*.${{ matrix.extension }})" | ||||
|         ANDROID_APK_PATH="${{ github.workspace }}/$VCMI_PACKAGE_FILE_NAME.${{ matrix.extension }}" | ||||
|         mv "$builtApkPath" "$ANDROID_APK_PATH" | ||||
|         echo "ANDROID_APK_PATH=$ANDROID_APK_PATH" >> $GITHUB_ENV | ||||
|     - name: Android artifacts | ||||
|       if: ${{ startsWith(matrix.platform, 'android') }} | ||||
|       uses: actions/upload-artifact@v4 | ||||
| @@ -262,7 +268,7 @@ jobs: | ||||
|         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} | ||||
|         path: | | ||||
|           ${{ env.ANDROID_APK_PATH }} | ||||
|            | ||||
|  | ||||
|     - name: Symbols | ||||
|       if: ${{ matrix.platform == 'msvc' }} | ||||
|       uses: actions/upload-artifact@v4 | ||||
| @@ -283,19 +289,17 @@ jobs: | ||||
|       if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' && matrix.platform != 'mingw-32' }} | ||||
|       continue-on-error: true | ||||
|       run: | | ||||
|         if cd '${{github.workspace}}/android/vcmi-app/build/outputs/apk/daily' ; then | ||||
|           mv '${{ env.ANDROID_APK_PATH }}' "$VCMI_PACKAGE_FILE_NAME.${{ matrix.extension }}" | ||||
|         else | ||||
|         if [ -z '${{ env.ANDROID_APK_PATH }}' ] ; then | ||||
|           cd '${{github.workspace}}/out/build/${{matrix.preset}}' | ||||
|         fi | ||||
|         source '${{github.workspace}}/CI/upload_package.sh' | ||||
|       env: | ||||
|         DEPLOY_RSA: ${{ secrets.DEPLOY_RSA }} | ||||
|         PACKAGE_EXTENSION: ${{ matrix.extension }} | ||||
|    | ||||
|  | ||||
|   # copy-pasted mostly | ||||
|   bundle_release: | ||||
|      | ||||
|  | ||||
|     needs: build | ||||
|     if: always() && github.ref == 'refs/heads/master' | ||||
|     strategy: | ||||
| @@ -303,7 +307,6 @@ jobs: | ||||
|         include: | ||||
|           - platform: android-32 | ||||
|             os: ubuntu-22.04 | ||||
|             extension: aab | ||||
|             preset: android-conan-ninja-release | ||||
|             conan_profile: android-32 | ||||
|             conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT | ||||
| @@ -367,13 +370,13 @@ jobs: | ||||
|       uses: actions/download-artifact@v4 | ||||
|       with: | ||||
|         name: Android JNI android-64 | ||||
|         path: ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs/ | ||||
|   | ||||
|         path: ${{ github.workspace }}/out/build/${{ matrix.preset }}/android-build/libs | ||||
|  | ||||
|     - name: Create Android package | ||||
|       run: | | ||||
|         cd android | ||||
|         cd out/build/${{ matrix.preset }}/android-build | ||||
|         ./gradlew bundleRelease --info | ||||
|         echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/android/vcmi-app/build/outputs/bundle/release/*.aab)" >> $GITHUB_ENV | ||||
|         echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/out/build/${{ matrix.preset }}/android-build/vcmi-app/build/outputs/bundle/release/*.aab)" >> $GITHUB_ENV | ||||
|       env: | ||||
|         ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} | ||||
|         ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} | ||||
|   | ||||
| @@ -403,7 +403,7 @@ void Nullkiller::makeTurn() | ||||
|  | ||||
| 		if(selectedTasks.empty()) | ||||
| 		{ | ||||
| 			return; | ||||
| 			selectedTasks.push_back(taskptr(Goals::Invalid())); | ||||
| 		} | ||||
|  | ||||
| 		bool hasAnySuccess = false; | ||||
|   | ||||
| @@ -26,9 +26,9 @@ bool ExploreNeighbourTile::operator==(const ExploreNeighbourTile & other) const | ||||
|  | ||||
| void ExploreNeighbourTile::accept(AIGateway * ai) | ||||
| { | ||||
| 	ExplorationHelper h(hero, ai->nullkiller.get()); | ||||
| 	ExplorationHelper h(hero, ai->nullkiller.get(), true); | ||||
|  | ||||
| 	for(int i = 0; i < tilesToExplore && hero->movementPointsRemaining() > 0; i++) | ||||
| 	for(int i = 0; i < tilesToExplore && ai->myCb->getObj(hero->id, false) && hero->movementPointsRemaining() > 0; i++) | ||||
| 	{ | ||||
| 		int3 pos = hero->visitablePos(); | ||||
| 		float value = 0; | ||||
| @@ -54,7 +54,14 @@ void ExploreNeighbourTile::accept(AIGateway * ai) | ||||
| 				} | ||||
| 			}); | ||||
|  | ||||
| 		if(!target.valid() || !ai->moveHeroToTile(target, hero)) | ||||
| 		if(!target.valid()) | ||||
| 		{ | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		auto danger = ai->nullkiller->pathfinder->getStorage()->evaluateDanger(target, hero, true); | ||||
|  | ||||
| 		if(danger > 0 || !ai->moveHeroToTile(target, hero)) | ||||
| 		{ | ||||
| 			return; | ||||
| 		} | ||||
|   | ||||
| @@ -24,8 +24,8 @@ namespace NKAI | ||||
|  | ||||
| using namespace Goals; | ||||
|  | ||||
| ExplorationHelper::ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai) | ||||
| 	:ai(ai), cbp(ai->cb.get()), hero(hero) | ||||
| ExplorationHelper::ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai, bool useCPathfinderAccessibility) | ||||
| 	:ai(ai), cbp(ai->cb.get()), hero(hero), useCPathfinderAccessibility(useCPathfinderAccessibility) | ||||
| { | ||||
| 	ts = cbp->getPlayerTeam(ai->playerID); | ||||
| 	sightRadius = hero->getSightRadius(); | ||||
| @@ -104,7 +104,7 @@ bool ExplorationHelper::scanMap() | ||||
|  | ||||
| 	if(!bestGoal->invalid()) | ||||
| 	{ | ||||
| 		return false; | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	allowDeadEndCancellation = false; | ||||
| @@ -222,7 +222,9 @@ bool ExplorationHelper::hasReachableNeighbor(const int3 & pos) const | ||||
| 		int3 tile = pos + dir; | ||||
| 		if(cbp->isInTheMap(tile)) | ||||
| 		{ | ||||
| 			auto isAccessible = ai->pathfinder->isTileAccessible(hero, tile); | ||||
| 			auto isAccessible = useCPathfinderAccessibility | ||||
| 				? ai->cb->getPathsInfo(hero)->getPathInfo(tile)->reachable() | ||||
| 				: ai->pathfinder->isTileAccessible(hero, tile); | ||||
|  | ||||
| 			if(isAccessible) | ||||
| 				return true; | ||||
|   | ||||
| @@ -34,9 +34,10 @@ private: | ||||
| 	const TeamState * ts; | ||||
| 	int3 ourPos; | ||||
| 	bool allowDeadEndCancellation; | ||||
| 	bool useCPathfinderAccessibility; | ||||
|  | ||||
| public: | ||||
| 	ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai); | ||||
| 	ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai, bool useCPathfinderAccessibility = false); | ||||
| 	Goals::TSubgoal makeComposition() const; | ||||
| 	bool scanSector(int scanRadius); | ||||
| 	bool scanMap(); | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| sudo apt-get update | ||||
| sudo apt-get install ninja-build | ||||
| echo "ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/25.2.9519653" >> $GITHUB_ENV | ||||
|  | ||||
| brew install ninja | ||||
|  | ||||
| mkdir ~/.conan ; cd ~/.conan | ||||
| curl -L "https://github.com/vcmi/vcmi-dependencies/releases/download/android-1.0/$DEPS_FILENAME.txz" \ | ||||
| curl -L "https://github.com/vcmi/vcmi-dependencies/releases/download/android-1.1/$DEPS_FILENAME.txz" \ | ||||
| 	| tar -xf - --xz | ||||
|   | ||||
							
								
								
									
										4
									
								
								CI/conan/android-32-ndk
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								CI/conan/android-32-ndk
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| include(android-32) | ||||
|  | ||||
| [tool_requires] | ||||
| android-ndk/r25c | ||||
							
								
								
									
										4
									
								
								CI/conan/android-64-ndk
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								CI/conan/android-64-ndk
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| include(android-64) | ||||
|  | ||||
| [tool_requires] | ||||
| android-ndk/r25c | ||||
| @@ -58,13 +58,22 @@ option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" O | ||||
| # Platform-specific options | ||||
|  | ||||
| if(ANDROID) | ||||
| 	set(ANDROID_TARGET_SDK_VERSION "33" CACHE STRING "Android target SDK version") | ||||
| 	set(ANDROIDDEPLOYQT_OPTIONS "" CACHE STRING "Additional androiddeployqt options separated by semi-colon") | ||||
| 	set(ANDROID_GRADLE_PROPERTIES "" CACHE STRING "Additional Gradle properties separated by semi-colon") | ||||
|  | ||||
| 	set(ENABLE_STATIC_LIBS ON) | ||||
| 	set(ENABLE_LAUNCHER OFF) | ||||
| 	set(ENABLE_LAUNCHER ON) | ||||
| else() | ||||
| 	option(ENABLE_STATIC_LIBS "Build library and all components such as AI statically" OFF) | ||||
| 	option(ENABLE_LAUNCHER "Enable compilation of launcher" ON) | ||||
| endif() | ||||
|  | ||||
| if(APPLE_IOS) | ||||
| 	set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix") | ||||
| 	set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen") | ||||
| endif() | ||||
|  | ||||
| if(APPLE_IOS OR ANDROID) | ||||
| 	set(ENABLE_MONOLITHIC_INSTALL OFF) | ||||
| 	set(ENABLE_SINGLE_APP_BUILD ON) | ||||
| @@ -100,11 +109,6 @@ if (ENABLE_STATIC_LIBS AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") | ||||
| 	set(CMAKE_POSITION_INDEPENDENT_CODE ON) | ||||
| endif() | ||||
|  | ||||
| if(APPLE_IOS) | ||||
| 	set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix") | ||||
| 	set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen") | ||||
| endif() | ||||
|  | ||||
| if(ENABLE_COLORIZED_COMPILER_OUTPUT) | ||||
| 	if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") | ||||
| 		add_compile_options(-fcolor-diagnostics) | ||||
| @@ -147,10 +151,6 @@ set(CMAKE_MODULE_PATH ${CMAKE_HOME_DIRECTORY}/cmake_modules ${PROJECT_SOURCE_DIR | ||||
|  | ||||
| include(VCMIUtils) | ||||
| include(VersionDefinition) | ||||
| if(ANDROID) | ||||
| 	set(VCMI_VERSION "${APP_SHORT_VERSION}") | ||||
| 	configure_file("android/GeneratedVersion.java.in" "${CMAKE_SOURCE_DIR}/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/GeneratedVersion.java" @ONLY) | ||||
| endif() | ||||
|  | ||||
| vcmi_print_important_variables() | ||||
|  | ||||
| @@ -575,8 +575,12 @@ elseif(APPLE) | ||||
| 	endif() | ||||
| elseif(ANDROID) | ||||
| 	include(GNUInstallDirs) | ||||
| 	set(LIB_DIR "jniLibs/${ANDROID_ABI}") | ||||
| 	set(DATA_DIR "assets") | ||||
| 	set(LIB_DIR "libs/${ANDROID_ABI}") | ||||
|  | ||||
| 	# required by Qt | ||||
| 	set(androidPackageSourceDir "${CMAKE_SOURCE_DIR}/android") | ||||
| 	set(androidQtBuildDir "${CMAKE_BINARY_DIR}/android-build") | ||||
| 	set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${androidQtBuildDir}/${LIB_DIR}") | ||||
| else() | ||||
| 	# includes lib path which determines where to install shared libraries (either /lib or /lib64) | ||||
| 	include(GNUInstallDirs) | ||||
| @@ -621,6 +625,13 @@ else() | ||||
| 	set(SCRIPTING_LIB_DIR "${LIB_DIR}/scripting") | ||||
| endif() | ||||
|  | ||||
| # common Qt paths | ||||
| if(ENABLE_LAUNCHER OR ENABLE_EDITOR) | ||||
| 	get_target_property(qmakePath Qt${QT_VERSION_MAJOR}::qmake IMPORTED_LOCATION) | ||||
| 	get_filename_component(qtDir "${qmakePath}/../../" ABSOLUTE) | ||||
| 	set(qtBinDir "${qtDir}/bin") | ||||
| endif() | ||||
|  | ||||
| ####################################### | ||||
| #        Add subdirectories           # | ||||
| ####################################### | ||||
| @@ -682,32 +693,15 @@ endif() | ||||
| ####################################### | ||||
|  | ||||
| if(ANDROID) | ||||
| 	string(REPLACE ";" "\n" ANDROID_GRADLE_PROPERTIES_MULTILINE "${ANDROID_GRADLE_PROPERTIES}") | ||||
| 	file(WRITE "${androidPackageSourceDir}/vcmi-app/gradle.properties" "signingRoot=${CMAKE_SOURCE_DIR}/CI/android\n${ANDROID_GRADLE_PROPERTIES_MULTILINE}") | ||||
|  | ||||
| 	if(ANDROID_STL MATCHES "_shared$") | ||||
| 		set(stlLibName "${CMAKE_SHARED_LIBRARY_PREFIX}${ANDROID_STL}${CMAKE_SHARED_LIBRARY_SUFFIX}") | ||||
| 		install(FILES "${CMAKE_SYSROOT}/usr/lib/${ANDROID_SYSROOT_LIB_SUBDIR}/${stlLibName}" | ||||
| 			DESTINATION ${LIB_DIR} | ||||
| 		) | ||||
| 	endif() | ||||
|  | ||||
| 	# zip internal assets - 'config' and 'Mods' dirs, save md5 of the zip | ||||
| 	install(CODE " | ||||
| 		cmake_path(ABSOLUTE_PATH CMAKE_INSTALL_PREFIX | ||||
| 			OUTPUT_VARIABLE absolute_install_prefix | ||||
| 		) | ||||
| 		set(absolute_data_dir \"\${absolute_install_prefix}/${DATA_DIR}\") | ||||
| 		file(MAKE_DIRECTORY \"\${absolute_data_dir}\") | ||||
|  | ||||
| 		set(internal_data_zip \"\${absolute_data_dir}/internalData.zip\") | ||||
| 		execute_process(COMMAND | ||||
| 			\"${CMAKE_COMMAND}\" -E tar c \"\${internal_data_zip}\" --format=zip -- config Mods | ||||
| 			WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\" | ||||
| 		) | ||||
|  | ||||
| 		file(MD5 \"\${internal_data_zip}\" internal_data_zip_md5) | ||||
| 		file(WRITE \"\${absolute_data_dir}/internalDataHash.txt\" | ||||
| 			\${internal_data_zip_md5} | ||||
| 		) | ||||
| 	") | ||||
| else() | ||||
| 	install(DIRECTORY config DESTINATION ${DATA_DIR}) | ||||
| 	if (ENABLE_CLIENT OR ENABLE_SERVER) | ||||
|   | ||||
| @@ -52,7 +52,7 @@ | ||||
|             "hidden": true, | ||||
|             "cacheVariables": { | ||||
|                 "ENABLE_LOBBY": "ON", | ||||
| 				"ENABLE_TEST": "ON", | ||||
|                 "ENABLE_TEST": "ON", | ||||
|                 "ENABLE_LUA": "ON" | ||||
|             } | ||||
|         }, | ||||
| @@ -294,6 +294,15 @@ | ||||
|             "cacheVariables": { | ||||
|                 "CMAKE_BUILD_TYPE": "RelWithDebInfo" | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             "name": "android-daily-release", | ||||
|             "displayName": "Android daily release", | ||||
|             "description": "VCMI Android daily build", | ||||
|             "inherits": "android-conan-ninja-release", | ||||
|             "cacheVariables": { | ||||
|                 "ANDROID_GRADLE_PROPERTIES": "applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily" | ||||
|             } | ||||
|         } | ||||
|     ], | ||||
|     "buildPresets": [ | ||||
| @@ -412,6 +421,11 @@ | ||||
|             "name": "android-conan-ninja-release", | ||||
|             "configurePreset": "android-conan-ninja-release", | ||||
|             "inherits": "default-release" | ||||
|         }, | ||||
|         { | ||||
|             "name": "android-daily-release", | ||||
|             "configurePreset": "android-daily-release", | ||||
|             "inherits": "android-conan-ninja-release" | ||||
|         } | ||||
|     ], | ||||
|     "testPresets": [ | ||||
|   | ||||
							
								
								
									
										4
									
								
								android/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								android/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,9 +1,11 @@ | ||||
| *.iml | ||||
| .gradle | ||||
| /local.properties | ||||
| .DS_Store | ||||
| /build | ||||
| /captures | ||||
| .externalNativeBuild | ||||
| .cxx | ||||
| local.properties | ||||
|  | ||||
| # generated by CMake build | ||||
| /vcmi-app/gradle.properties | ||||
|   | ||||
							
								
								
									
										98
									
								
								android/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								android/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     package="eu.vcmi.vcmi"> | ||||
|  | ||||
|     <!-- %%INSERT_PERMISSIONS --> | ||||
|     <uses-permission android:name="android.permission.VIBRATE" /> | ||||
|  | ||||
|     <!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application. | ||||
|          Remove the comment if you do not require these default permissions. --> | ||||
|     <!-- %%INSERT_PERMISSIONS_DISABLED --> | ||||
|  | ||||
|     <!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application. | ||||
|          Remove the comment if you do not require these default features. --> | ||||
|     <!-- %%INSERT_FEATURES --> | ||||
|  | ||||
| 	<supports-screens | ||||
|         android:largeScreens="true" | ||||
|         android:xlargeScreens="true" /> | ||||
|     <application | ||||
|         android:name="org.qtproject.qt5.android.bindings.QtApplication" | ||||
|         android:hardwareAccelerated="true" | ||||
|         android:hasFragileUserData="true" | ||||
|         android:allowBackup="false" | ||||
|         android:installLocation="auto" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="${applicationLabel}" | ||||
|         android:testOnly="false" | ||||
|         android:supportsRtl="true" | ||||
|         android:usesCleartextTraffic="false"> | ||||
| 		<activity | ||||
| 			android:name=".ActivityLauncher" | ||||
| 			android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" | ||||
| 			android:exported="true" | ||||
|             android:screenOrientation="sensorLandscape"> | ||||
| 			<intent-filter> | ||||
| 				<action android:name="android.intent.action.MAIN"/> | ||||
| 				<category android:name="android.intent.category.LAUNCHER"/> | ||||
| 			</intent-filter> | ||||
|  | ||||
| 			<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/> | ||||
| 			<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/> | ||||
| 			<meta-data android:name="android.app.repository" android:value="default"/> | ||||
| 			<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/> | ||||
| 			<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/> | ||||
| 			<!-- Deploy Qt libs as part of package --> | ||||
| 			<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/> | ||||
| 			<!-- Run with local libs --> | ||||
| 			<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/> | ||||
| 			<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/> | ||||
| 			<meta-data android:name="android.app.load_local_libs_resource_id" android:resource="@array/load_local_libs"/> | ||||
| 			<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/> | ||||
| 			<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/> | ||||
|  | ||||
| 			<!--  Messages maps --> | ||||
| 			<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/> | ||||
| 			<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/> | ||||
| 			<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/> | ||||
| 			<meta-data android:value="@string/unsupported_android_version" android:name="android.app.unsupported_android_version"/> | ||||
| 			<!--  Messages maps --> | ||||
|  | ||||
| 			<!-- Background running --> | ||||
| 			<!-- Warning: changing this value to true may cause unexpected crashes if the | ||||
| 						  application still try to draw after | ||||
| 						  "applicationStateChanged(Qt::ApplicationSuspended)" | ||||
| 						  signal is sent! --> | ||||
| 			<meta-data android:name="android.app.background_running" android:value="false"/> | ||||
| 			<!-- Background running --> | ||||
|  | ||||
| 			<!-- auto screen scale factor --> | ||||
| 			<meta-data android:name="android.app.auto_screen_scale_factor" android:value="false"/> | ||||
| 			<!-- auto screen scale factor --> | ||||
|  | ||||
| 			<!-- extract android style --> | ||||
| 			<!-- available android:values : | ||||
| 				* default - In most cases this will be the same as "full", but it can also be something else if needed, e.g., for compatibility reasons | ||||
| 				* full - useful QWidget & Quick Controls 1 apps | ||||
| 				* minimal - useful for Quick Controls 2 apps, it is much faster than "full" | ||||
| 				* none - useful for apps that don't use any of the above Qt modules | ||||
| 				--> | ||||
| 			<meta-data android:name="android.app.extract_android_style" android:value="none"/> | ||||
| 			<!-- extract android style --> | ||||
| 		</activity> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".VcmiSDLActivity" | ||||
|             android:configChanges="keyboardHidden|orientation|screenSize" | ||||
|             android:label="@string/app_name" | ||||
|             android:launchMode="singleTop" | ||||
|             android:screenOrientation="sensorLandscape" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".ServerService" | ||||
|             android:process="eu.vcmi.vcmi.srv" | ||||
|             android:description="@string/server_name" | ||||
|             android:exported="false"/> | ||||
|     </application> | ||||
|  | ||||
| </manifest> | ||||
| @@ -1,9 +0,0 @@ | ||||
| package eu.vcmi.vcmi.util; | ||||
|  | ||||
| /** | ||||
|  * Generated via cmake | ||||
|  */ | ||||
| public class GeneratedVersion | ||||
| { | ||||
|     public static final String VCMI_VERSION = "@VCMI_VERSION@"; | ||||
| } | ||||
							
								
								
									
										17
									
								
								android/androiddeployqt.json.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								android/androiddeployqt.json.in
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| { | ||||
| 	"android-min-sdk-version": "@CMAKE_ANDROID_API@", | ||||
| 	"android-package-source-directory": "@androidPackageSourceDir@", | ||||
| 	"android-target-sdk-version": "@ANDROID_TARGET_SDK_VERSION@", | ||||
| 	"application-binary": "vcmiclient", | ||||
| 	"architectures": { | ||||
| 		"@ANDROID_ABI@": "@ANDROID_SYSROOT_LIB_SUBDIR@" | ||||
| 	}, | ||||
| 	"ndk": "@CMAKE_ANDROID_NDK@", | ||||
| 	"ndk-host": "@ANDROID_HOST_TAG@", | ||||
| 	"qt": "@qtDir@", | ||||
| 	"sdk": "@androidSdkDir@", | ||||
| 	"sdkBuildToolsRevision": "31.0.0", | ||||
| 	"stdcpp-path": "@ANDROID_TOOLCHAIN_ROOT@/sysroot/usr/lib/", | ||||
| 	"tool-prefix": "llvm", | ||||
| 	"toolchain-prefix": "llvm" | ||||
| } | ||||
| @@ -1,4 +1,3 @@ | ||||
| ext { | ||||
| 	// these values will be retrieved during gradle build | ||||
| 	gitInfoVcmi = "none" | ||||
| } | ||||
|   | ||||
| @@ -7,13 +7,18 @@ | ||||
| # Specifies the JVM arguments used for the daemon process. | ||||
| # The setting is particularly useful for tweaking memory settings. | ||||
| org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 | ||||
|  | ||||
| # When configured, Gradle will run in incubating parallel mode. | ||||
| # This option should only be used with decoupled projects. More details, visit | ||||
| # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects | ||||
| # org.gradle.parallel=true | ||||
| org.gradle.parallel=true | ||||
|  | ||||
| # AndroidX package structure to make it clearer which packages are bundled with the | ||||
| # Android operating system, and which are packaged with your app"s APK | ||||
| # https://developer.android.com/topic/libraries/support-library/androidx-rn | ||||
| android.useAndroidX=true | ||||
|  | ||||
| # Automatically convert third-party libraries to use AndroidX | ||||
| android.enableJetifier=true | ||||
|  | ||||
| # Qt-generated properties | ||||
|   | ||||
							
								
								
									
										8
									
								
								android/vcmi-app/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								android/vcmi-app/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +0,0 @@ | ||||
| /build | ||||
|  | ||||
| # generated by CMake build | ||||
| /src/main/assets/internalData.zip | ||||
| /src/main/assets/internalDataHash.txt | ||||
| /src/main/java/eu/vcmi/vcmi/util/GeneratedVersion.java | ||||
| /src/main/jniLibs | ||||
| /src/main/res/raw/authors.txt | ||||
| @@ -1,20 +1,49 @@ | ||||
| plugins { | ||||
| 	id 'com.android.application' | ||||
| } | ||||
| apply plugin: 'com.android.application' | ||||
|  | ||||
| android { | ||||
| 	compileSdk 33 | ||||
| 	/******************************************************* | ||||
| 	 * The following variables: | ||||
| 	 * - androidBuildToolsVersion, | ||||
| 	 * - androidCompileSdkVersion | ||||
| 	 * - qt5AndroidDir - holds the path to qt android files | ||||
| 	 *                   needed to build any Qt application | ||||
| 	 *                   on Android. | ||||
| 	 * | ||||
| 	 * are defined in gradle.properties file. This file is | ||||
| 	 * updated by QtCreator and androiddeployqt tools. | ||||
| 	 * Changing them manually might break the compilation! | ||||
| 	 *******************************************************/ | ||||
|  | ||||
| 	ndkVersion '25.2.9519653' | ||||
|  | ||||
| 	// Extract native libraries from the APK | ||||
| 	packagingOptions.jniLibs.useLegacyPackaging true | ||||
|  | ||||
| 	defaultConfig { | ||||
| 		applicationId "is.xyz.vcmi" | ||||
| 		minSdk 19 | ||||
| 		targetSdk 33 | ||||
|  | ||||
| 		compileSdk = androidCompileSdkVersion.takeAfter("-") as Integer // has "android-" prepended | ||||
| 		minSdk = qtMinSdkVersion as Integer | ||||
| 		targetSdk = qtTargetSdkVersion as Integer // ANDROID_TARGET_SDK_VERSION in the CMake project | ||||
|  | ||||
| 		versionCode 1600 | ||||
| 		versionName "1.6.0" | ||||
|  | ||||
| 		setProperty("archivesBaseName", "vcmi") | ||||
| 	} | ||||
|  | ||||
| 	sourceSets { | ||||
| 		main { | ||||
| 			// Qt requires these to be in the android project root | ||||
| 			manifest.srcFile '../AndroidManifest.xml' | ||||
| 			jniLibs.srcDirs = ['../libs'] | ||||
|  | ||||
| 			java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] | ||||
| 			aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] | ||||
| 			res.srcDirs = [qt5AndroidDir + '/res', 'src/main/res', '../res'] | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	signingConfigs { | ||||
| 		releaseSigning | ||||
| 		dailySigning | ||||
| @@ -36,27 +65,18 @@ android { | ||||
| 		release { | ||||
| 			minifyEnabled false | ||||
| 			zipAlignEnabled true | ||||
| 			signingConfig signingConfigs.releaseSigning | ||||
| 			applicationIdSuffix = project.findProperty('applicationIdSuffix') | ||||
| 			signingConfig = signingConfigs[project.findProperty('signingConfig') ?: 'releaseSigning'] | ||||
| 			proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' | ||||
| 			manifestPlaceholders = [ | ||||
| 				applicationLabel: '@string/app_name', | ||||
| 				applicationLabel: project.findProperty('applicationLabel') ?: 'VCMI', | ||||
| 			] | ||||
| 			ndk { | ||||
| 				debugSymbolLevel 'full' | ||||
| 			} | ||||
| 		} | ||||
| 		daily { | ||||
| 			initWith release | ||||
| 			applicationIdSuffix '.daily' | ||||
| 			signingConfig signingConfigs.dailySigning | ||||
| 			manifestPlaceholders = [ | ||||
| 				applicationLabel: 'VCMI daily', | ||||
| 			] | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	applicationVariants.all { variant -> RenameOutput(project.archivesBaseName, variant) } | ||||
|  | ||||
| 	tasks.withType(JavaCompile) { | ||||
| 		options.compilerArgs += ["-Xlint:deprecation"] | ||||
| 	} | ||||
| @@ -66,30 +86,9 @@ android { | ||||
| 		targetCompatibility JavaVersion.VERSION_1_8 | ||||
| 	} | ||||
|  | ||||
| 	buildFeatures { | ||||
| 		viewBinding true | ||||
| 		dataBinding true | ||||
| 	} | ||||
| } | ||||
|  | ||||
| def RenameOutput(final baseName, final variant) { | ||||
| 	final def buildTaskId = System.getenv("GITHUB_RUN_ID") | ||||
|  | ||||
| 	ResolveGitInfo() | ||||
|  | ||||
| 	def name = baseName + "-" + ext.gitInfoVcmi | ||||
|  | ||||
| 	if (buildTaskId != null && !buildTaskId.isEmpty()) { | ||||
| 		name = buildTaskId + "-" + name | ||||
| 	} | ||||
|  | ||||
| 	if (!variant.buildType.name != "release") { | ||||
| 		name += "-" + variant.buildType.name | ||||
| 	} | ||||
|  | ||||
| 	variant.outputs.each { output -> | ||||
| 		def oldPath = output.outputFile.getAbsolutePath() | ||||
| 		output.outputFileName = name + oldPath.substring(oldPath.lastIndexOf(".")) | ||||
| 	// Do not compress Qt binary resources file | ||||
| 	aaptOptions { | ||||
| 		noCompress 'rcc' | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -111,16 +110,6 @@ def CommandOutput(final cmd, final arguments, final cwd) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| def ResolveGitInfo() { | ||||
| 	if (ext.gitInfoVcmi != "none") { | ||||
| 		return | ||||
| 	} | ||||
| 	ext.gitInfoVcmi = | ||||
| 		CommandOutput("git", ["log", "-1", "--pretty=%D", "--decorate-refs=refs/remotes/origin/*"], ".").replace("origin/", "").replace(", HEAD", "").replaceAll("[^a-zA-Z0-9\\-_]", "_") + | ||||
| 		"-" + | ||||
| 		CommandOutput("git", ["describe", "--match=", "--always", "--abbrev=7"], ".") | ||||
| } | ||||
|  | ||||
| def SigningPropertiesPath(final basePath, final signingConfigKey) { | ||||
| 	return file("${basePath}/${signingConfigKey}.properties") | ||||
| } | ||||
| @@ -130,9 +119,8 @@ def SigningKeystorePath(final basePath, final keystoreFileName) { | ||||
| } | ||||
|  | ||||
| def LoadSigningConfig(final signingConfigKey) { | ||||
| 	final def projectRoot = "${project.projectDir}/../../CI/android" | ||||
| 	final def props = new Properties() | ||||
| 	final def propFile = SigningPropertiesPath(projectRoot, signingConfigKey) | ||||
| 	final def propFile = SigningPropertiesPath(signingRoot, signingConfigKey) | ||||
| 	 | ||||
| 	def signingConfig = android.signingConfigs.getAt(signingConfigKey) | ||||
| 	 | ||||
| @@ -143,7 +131,7 @@ def LoadSigningConfig(final signingConfigKey) { | ||||
| 			&& props.containsKey('STORE_FILE') | ||||
| 			&& props.containsKey('KEY_ALIAS')) { | ||||
|  | ||||
| 			signingConfig.storeFile = SigningKeystorePath(projectRoot, props['STORE_FILE']) | ||||
| 			signingConfig.storeFile = SigningKeystorePath(signingRoot, props['STORE_FILE']) | ||||
| 			signingConfig.storePassword = props['STORE_PASSWORD'] | ||||
| 			signingConfig.keyAlias = props['KEY_ALIAS'] | ||||
| 			 | ||||
| @@ -167,9 +155,7 @@ def LoadSigningConfig(final signingConfigKey) { | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
| 	implementation 'androidx.appcompat:appcompat:1.2.0' | ||||
| 	implementation 'com.google.android.material:material:1.3.0' | ||||
| 	implementation 'androidx.constraintlayout:constraintlayout:2.0.4' | ||||
| 	implementation 'com.google.android.gms:play-services-base:18.2.0' | ||||
| 	implementation 'com.google.android.gms:play-services-basement:18.1.0' | ||||
| 	implementation fileTree(dir: '../libs', include: ['*.jar', '*.aar']) | ||||
| 	implementation 'androidx.annotation:annotation:1.7.1' | ||||
| 	implementation 'androidx.documentfile:documentfile:1.0.1' | ||||
| } | ||||
|   | ||||
| @@ -1,54 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     package="eu.vcmi.vcmi"> | ||||
|  | ||||
|     <uses-permission android:name="android.permission.INTERNET" /> | ||||
|     <uses-permission android:name="android.permission.VIBRATE" /> | ||||
|  | ||||
|     <application | ||||
|         android:extractNativeLibs="true" | ||||
|         android:hardwareAccelerated="true" | ||||
|         android:hasFragileUserData="true" | ||||
|         android:allowBackup="false" | ||||
|         android:installLocation="auto" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="${applicationLabel}" | ||||
|         android:testOnly="false" | ||||
|         android:supportsRtl="true" | ||||
|         android:theme="@style/Theme.VCMI"> | ||||
|         <activity | ||||
|             android:exported="true" | ||||
|             android:name=".ActivityLauncher" | ||||
|             android:screenOrientation="sensorLandscape"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.LAUNCHER" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ActivityError" | ||||
|             android:screenOrientation="sensorLandscape" /> | ||||
|         <activity | ||||
|             android:name=".ActivityMods" | ||||
|             android:screenOrientation="sensorLandscape" /> | ||||
|         <activity | ||||
|             android:name=".ActivityAbout" | ||||
|             android:screenOrientation="sensorLandscape" /> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".VcmiSDLActivity" | ||||
|             android:configChanges="keyboardHidden|orientation|screenSize" | ||||
|             android:label="@string/app_name" | ||||
|             android:launchMode="singleTop" | ||||
|             android:screenOrientation="sensorLandscape" | ||||
|             android:theme="@style/Theme.VCMI.Full" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".ServerService" | ||||
|             android:process="eu.vcmi.vcmi.srv" | ||||
|             android:description="@string/server_name" | ||||
|             android:exported="false"/> | ||||
|     </application> | ||||
|  | ||||
| </manifest> | ||||
| @@ -1,94 +0,0 @@ | ||||
| package eu.vcmi.vcmi; | ||||
|  | ||||
| import android.content.ActivityNotFoundException; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.core.content.ContextCompat; | ||||
| import android.text.SpannableString; | ||||
| import android.text.Spanned; | ||||
| import android.text.style.ForegroundColorSpan; | ||||
| import android.text.style.UnderlineSpan; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import eu.vcmi.vcmi.content.DialogAuthors; | ||||
| import eu.vcmi.vcmi.util.GeneratedVersion; | ||||
| import eu.vcmi.vcmi.util.Utils; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ActivityAbout extends ActivityWithToolbar | ||||
| { | ||||
|     private static final String DIALOG_AUTHORS_TAG = "DIALOG_AUTHORS_TAG"; | ||||
|  | ||||
|     @Override | ||||
|     protected void onCreate(@Nullable final Bundle savedInstanceState) | ||||
|     { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_about); | ||||
|         initToolbar(R.string.about_title); | ||||
|  | ||||
|         initControl(R.id.about_version_app, getString(R.string.about_version_app, GeneratedVersion.VCMI_VERSION)); | ||||
|         initControl(R.id.about_version_launcher, getString(R.string.about_version_launcher, Utils.appVersionName(this))); | ||||
|         initControlUrl(R.id.about_link_portal, R.string.about_links_main, R.string.url_project_page, this::onUrlPressed); | ||||
|         initControlUrl(R.id.about_link_repo_main, R.string.about_links_repo, R.string.url_project_repo, this::onUrlPressed); | ||||
|         initControlUrl(R.id.about_link_repo_launcher, R.string.about_links_repo_launcher, R.string.url_launcher_repo, this::onUrlPressed); | ||||
|         initControlBtn(R.id.about_btn_authors, this::onBtnAuthorsPressed); | ||||
|         initControlUrl(R.id.about_btn_privacy, R.string.about_btn_privacy, R.string.url_launcher_privacy, this::onUrlPressed); | ||||
|     } | ||||
|  | ||||
|     private void initControlBtn(final int viewId, final View.OnClickListener callback) | ||||
|     { | ||||
|         findViewById(viewId).setOnClickListener(callback); | ||||
|     } | ||||
|  | ||||
|     private void initControlUrl(final int textViewResId, final int baseTextRes, final int urlTextRes, final IInternalUrlCallback callback) | ||||
|     { | ||||
|         final TextView ctrl = (TextView) findViewById(textViewResId); | ||||
|         final String urlText = getString(urlTextRes); | ||||
|         final String fullText = getString(baseTextRes, urlText); | ||||
|  | ||||
|         ctrl.setText(decoratedLinkText(fullText, fullText.indexOf(urlText), fullText.length())); | ||||
|         ctrl.setOnClickListener(v -> callback.onPressed(urlText)); | ||||
|     } | ||||
|  | ||||
|     private Spanned decoratedLinkText(final String rawText, final int start, final int end) | ||||
|     { | ||||
|         final SpannableString spannableString = new SpannableString(rawText); | ||||
|         spannableString.setSpan(new UnderlineSpan(), start, end, 0); | ||||
|         spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(this, R.color.accent)), start, end, 0); | ||||
|         return spannableString; | ||||
|     } | ||||
|  | ||||
|     private void initControl(final int textViewResId, final String text) | ||||
|     { | ||||
|         ((TextView) findViewById(textViewResId)).setText(text); | ||||
|     } | ||||
|  | ||||
|     private void onBtnAuthorsPressed(final View v) | ||||
|     { | ||||
|         final DialogAuthors dialogAuthors = new DialogAuthors(); | ||||
|         dialogAuthors.show(getSupportFragmentManager(), DIALOG_AUTHORS_TAG); | ||||
|     } | ||||
|  | ||||
|     private void onUrlPressed(final String url) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); | ||||
|         } | ||||
|         catch (final ActivityNotFoundException ignored) | ||||
|         { | ||||
|             Toast.makeText(this, R.string.about_error_opening_url, Toast.LENGTH_LONG).show(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private interface IInternalUrlCallback | ||||
|     { | ||||
|         void onPressed(final String link); | ||||
|     } | ||||
| } | ||||
| @@ -1,58 +0,0 @@ | ||||
| package eu.vcmi.vcmi; | ||||
|  | ||||
| import android.os.Bundle; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
|  | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
| import eu.vcmi.vcmi.util.SharedPrefs; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public abstract class ActivityBase extends AppCompatActivity | ||||
| { | ||||
|     protected SharedPrefs mPrefs; | ||||
|  | ||||
|     @Override | ||||
|     protected void onCreate(@Nullable final Bundle savedInstanceState) | ||||
|     { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setupExceptionHandler(); | ||||
|         mPrefs = new SharedPrefs(this); | ||||
|     } | ||||
|  | ||||
|     private void setupExceptionHandler() | ||||
|     { | ||||
|         final Thread.UncaughtExceptionHandler prevHandler = Thread.getDefaultUncaughtExceptionHandler(); | ||||
|         if (prevHandler != null && !(prevHandler instanceof VCMIExceptionHandler)) // no need to recreate it if it's already setup | ||||
|         { | ||||
|             Thread.setDefaultUncaughtExceptionHandler(new VCMIExceptionHandler(prevHandler)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static class VCMIExceptionHandler implements Thread.UncaughtExceptionHandler | ||||
|     { | ||||
|         private Thread.UncaughtExceptionHandler mPrevHandler; | ||||
|  | ||||
|         private VCMIExceptionHandler(final Thread.UncaughtExceptionHandler prevHandler) | ||||
|         { | ||||
|             mPrevHandler = prevHandler; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void uncaughtException(final Thread thread, final Throwable throwable) | ||||
|         { | ||||
|             Log.e(this, "Unhandled exception", throwable); // to save the exception to file before crashing | ||||
|  | ||||
|             if (mPrevHandler != null && !(mPrevHandler instanceof VCMIExceptionHandler)) | ||||
|             { | ||||
|                 mPrevHandler.uncaughtException(thread, throwable); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 System.exit(1); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,48 +0,0 @@ | ||||
| package eu.vcmi.vcmi; | ||||
|  | ||||
| import android.content.Intent; | ||||
| import android.os.Bundle; | ||||
| import androidx.annotation.Nullable; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ActivityError extends ActivityWithToolbar | ||||
| { | ||||
|     public static final String ARG_ERROR_MSG = "ActivityError.msg"; | ||||
|  | ||||
|     @Override | ||||
|     protected void onCreate(@Nullable final Bundle savedInstanceState) | ||||
|     { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_error); | ||||
|         initToolbar(R.string.launcher_title); | ||||
|  | ||||
|         final View btnTryAgain = findViewById(R.id.error_btn_try_again); | ||||
|         btnTryAgain.setOnClickListener(new OnErrorRetryPressed()); | ||||
|  | ||||
|         final Bundle extras = getIntent().getExtras(); | ||||
|         if (extras != null) | ||||
|         { | ||||
|             final String errorMessage = extras.getString(ARG_ERROR_MSG); | ||||
|             final TextView errorMessageView = (TextView) findViewById(R.id.error_message); | ||||
|             if (errorMessage != null) | ||||
|             { | ||||
|                 errorMessageView.setText(errorMessage); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private class OnErrorRetryPressed implements View.OnClickListener | ||||
|     { | ||||
|         @Override | ||||
|         public void onClick(final View v) | ||||
|         { | ||||
|             // basically restarts main activity | ||||
|             startActivity(new Intent(ActivityError.this, ActivityLauncher.class)); | ||||
|             finish(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,307 +1,62 @@ | ||||
| package eu.vcmi.vcmi; | ||||
|  | ||||
| import android.Manifest; | ||||
| import android.app.Activity; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import android.os.Bundle; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
| import org.json.JSONObject; | ||||
| import java.io.File; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import android.os.Environment; | ||||
| import android.provider.DocumentsContract; | ||||
|  | ||||
| import eu.vcmi.vcmi.content.AsyncLauncherInitialization; | ||||
| import eu.vcmi.vcmi.settings.AdventureAiController; | ||||
| import eu.vcmi.vcmi.settings.LanguageSettingController; | ||||
| import eu.vcmi.vcmi.settings.CopyDataController; | ||||
| import eu.vcmi.vcmi.settings.ExportDataController; | ||||
| import eu.vcmi.vcmi.settings.LauncherSettingController; | ||||
| import eu.vcmi.vcmi.settings.ModsBtnController; | ||||
| import eu.vcmi.vcmi.settings.MusicSettingController; | ||||
| import eu.vcmi.vcmi.settings.PointerModeSettingController; | ||||
| import eu.vcmi.vcmi.settings.PointerMultiplierSettingController; | ||||
| import eu.vcmi.vcmi.settings.ScreenScaleSettingController; | ||||
| import eu.vcmi.vcmi.settings.ScreenScaleSettingDialog; | ||||
| import eu.vcmi.vcmi.settings.SoundSettingController; | ||||
| import eu.vcmi.vcmi.settings.StartGameController; | ||||
| import androidx.annotation.Nullable; | ||||
|  | ||||
| import java.io.File; | ||||
|  | ||||
| import eu.vcmi.vcmi.VcmiSDLActivity; | ||||
| import eu.vcmi.vcmi.util.FileUtil; | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
| import eu.vcmi.vcmi.util.SharedPrefs; | ||||
|  | ||||
| import org.libsdl.app.SDL; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ActivityLauncher extends ActivityWithToolbar | ||||
| public class ActivityLauncher extends org.qtproject.qt5.android.bindings.QtActivity | ||||
| { | ||||
|     public static final int PERMISSIONS_REQ_CODE = 123; | ||||
|     private static final int PICK_EXTERNAL_VCMI_DATA_TO_COPY = 1; | ||||
|  | ||||
|     private final List<LauncherSettingController<?, ?>> mActualSettings = new ArrayList<>(); | ||||
|     private View mProgress; | ||||
|     private TextView mErrorMessage; | ||||
|     private Config mConfig; | ||||
|     private LauncherSettingController<String, Config> mCtrlLanguage; | ||||
|     private LauncherSettingController<PointerModeSettingController.PointerMode, Config> mCtrlPointerMode; | ||||
|     private LauncherSettingController<Void, Void> mCtrlStart; | ||||
|     private LauncherSettingController<Float, Config> mCtrlPointerMulti; | ||||
|     private LauncherSettingController<ScreenScaleSettingController.ScreenScale, Config> mCtrlScreenScale; | ||||
|     private LauncherSettingController<Integer, Config> mCtrlSoundVol; | ||||
|     private LauncherSettingController<Integer, Config> mCtrlMusicVol; | ||||
|     private LauncherSettingController<String, Config> mAiController; | ||||
|     private CopyDataController mCtrlCopy; | ||||
|     private ExportDataController mCtrlExport; | ||||
|  | ||||
|     private final AsyncLauncherInitialization.ILauncherCallbacks mInitCallbacks = new AsyncLauncherInitialization.ILauncherCallbacks() | ||||
|     { | ||||
|         @Override | ||||
|         public Activity ctx() | ||||
|         { | ||||
|             return ActivityLauncher.this; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public SharedPrefs prefs() | ||||
|         { | ||||
|             return mPrefs; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onInitSuccess() | ||||
|         { | ||||
|             loadConfigFile(); | ||||
|             mCtrlStart.show(); | ||||
|             mCtrlCopy.show(); | ||||
|             mCtrlExport.show(); | ||||
|             for (LauncherSettingController<?, ?> setting: mActualSettings) { | ||||
|                 setting.show(); | ||||
|             } | ||||
|             mErrorMessage.setVisibility(View.GONE); | ||||
|             mProgress.setVisibility(View.GONE); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onInitFailure(final AsyncLauncherInitialization.InitResult result) | ||||
|         { | ||||
|             mCtrlCopy.show(); | ||||
|             if (result.mFailSilently) | ||||
|             { | ||||
|                 return; | ||||
|             } | ||||
|             ActivityLauncher.this.onInitFailure(result); | ||||
|         } | ||||
|     }; | ||||
|     public boolean justLaunched = true; | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(final Bundle savedInstanceState) | ||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) | ||||
|     { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         justLaunched = savedInstanceState == null; | ||||
|         SDL.setContext(this); | ||||
|     } | ||||
|  | ||||
|         if (savedInstanceState == null) // only clear the log if this is initial onCreate and not config change | ||||
|     @Override | ||||
|     protected void onActivityResult(int requestCode, int resultCode, Intent resultData) | ||||
|     { | ||||
|         if (requestCode == PICK_EXTERNAL_VCMI_DATA_TO_COPY && resultCode == Activity.RESULT_OK) | ||||
|         { | ||||
|             Log.init(); | ||||
|         } | ||||
|  | ||||
|         Log.i(this, "Starting launcher"); | ||||
|  | ||||
|         setContentView(R.layout.activity_launcher); | ||||
|         initToolbar(R.string.launcher_title, true); | ||||
|  | ||||
|         mProgress = findViewById(R.id.launcher_progress); | ||||
|         mErrorMessage = (TextView) findViewById(R.id.launcher_error); | ||||
|         mErrorMessage.setVisibility(View.GONE); | ||||
|  | ||||
|         ((TextView) findViewById(R.id.launcher_version_info)).setText(getString(R.string.launcher_version, BuildConfig.VERSION_NAME)); | ||||
|  | ||||
|         initSettingsGui(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onStart() | ||||
|     { | ||||
|         super.onStart(); | ||||
|         new AsyncLauncherInitialization(mInitCallbacks).execute((Void) null); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBackPressed() | ||||
|     { | ||||
|         saveConfig(); | ||||
|         super.onBackPressed(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onCreateOptionsMenu(final Menu menu) | ||||
|     { | ||||
|         getMenuInflater().inflate(R.menu.menu_launcher, menu); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(final MenuItem item) | ||||
|     { | ||||
|         if (item.getItemId() == R.id.menu_launcher_about) | ||||
|         { | ||||
|             startActivity(new Intent(this, ActivityAbout.class)); | ||||
|             return true; | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onActivityResult(int requestCode, int resultCode, Intent resultData) | ||||
|     { | ||||
|         if(requestCode == CopyDataController.PICK_EXTERNAL_VCMI_DATA_TO_COPY | ||||
|             && resultCode == Activity.RESULT_OK) | ||||
|         { | ||||
|             Uri uri; | ||||
|  | ||||
|             if (resultData != null) | ||||
|             { | ||||
|                 uri = resultData.getData(); | ||||
|  | ||||
|                 mCtrlCopy.copyData(uri); | ||||
|             } | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if(requestCode == ExportDataController.PICK_DIRECTORY_TO_EXPORT | ||||
|                 && resultCode == Activity.RESULT_OK) | ||||
|         { | ||||
|             Uri uri = null; | ||||
|             if (resultData != null) | ||||
|             { | ||||
|                 uri = resultData.getData(); | ||||
|  | ||||
|                 mCtrlExport.copyData(uri); | ||||
|             } | ||||
|  | ||||
|             if (resultData != null && FileUtil.copyData(resultData.getData(), this)) | ||||
|                 NativeMethods.heroesDataUpdate(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         super.onActivityResult(requestCode, resultCode, resultData); | ||||
|     } | ||||
|  | ||||
|     public void requestStoragePermissions() | ||||
|     public void copyHeroesData() | ||||
|     { | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) | ||||
|         { | ||||
|             requestPermissions( | ||||
|                     new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, | ||||
|                     PERMISSIONS_REQ_CODE); | ||||
|         } | ||||
|         Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); | ||||
|         intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, | ||||
|             Uri.fromFile(new File(Environment.getExternalStorageDirectory(), "vcmi-data")) | ||||
|         ); | ||||
|         startActivityForResult(intent, PICK_EXTERNAL_VCMI_DATA_TO_COPY); | ||||
|     } | ||||
|  | ||||
|     private void initSettingsGui() | ||||
|     public void onLaunchGameBtnPressed() | ||||
|     { | ||||
|         mCtrlStart = new StartGameController(this, v -> onLaunchGameBtnPressed()).init(R.id.launcher_btn_start); | ||||
|         (mCtrlCopy = new CopyDataController(this)).init(R.id.launcher_btn_copy); | ||||
|         (mCtrlExport = new ExportDataController(this)).init(R.id.launcher_btn_export); | ||||
|         new ModsBtnController(this, v -> startActivity(new Intent(ActivityLauncher.this, ActivityMods.class))).init(R.id.launcher_btn_mods); | ||||
|         mCtrlLanguage = new LanguageSettingController(this).init(R.id.launcher_btn_cp, mConfig); | ||||
|         mCtrlPointerMode = new PointerModeSettingController(this).init(R.id.launcher_btn_pointer_mode, mConfig); | ||||
|         mCtrlPointerMulti = new PointerMultiplierSettingController(this).init(R.id.launcher_btn_pointer_multi, mConfig); | ||||
|         mCtrlScreenScale = new ScreenScaleSettingController(this).init(R.id.launcher_btn_scale, mConfig); | ||||
|         mCtrlSoundVol = new SoundSettingController(this).init(R.id.launcher_btn_volume_sound, mConfig); | ||||
|         mCtrlMusicVol = new MusicSettingController(this).init(R.id.launcher_btn_volume_music, mConfig); | ||||
|         mAiController = new AdventureAiController(this).init(R.id.launcher_btn_adventure_ai, mConfig); | ||||
|  | ||||
|         mActualSettings.clear(); | ||||
|         mActualSettings.add(mCtrlLanguage); | ||||
|         mActualSettings.add(mCtrlPointerMode); | ||||
|         mActualSettings.add(mCtrlPointerMulti); | ||||
|         mActualSettings.add(mCtrlScreenScale); | ||||
|         mActualSettings.add(mCtrlSoundVol); | ||||
|         mActualSettings.add(mCtrlMusicVol); | ||||
|         mActualSettings.add(mAiController); | ||||
|  | ||||
|         mCtrlStart.hide(); // start is initially hidden, until we confirm that everything is okay via AsyncLauncherInitialization | ||||
|         mCtrlCopy.hide(); | ||||
|         mCtrlExport.hide(); | ||||
|     } | ||||
|  | ||||
|     private void onLaunchGameBtnPressed() | ||||
|     { | ||||
|         saveConfig(); | ||||
|         startActivity(new Intent(ActivityLauncher.this, VcmiSDLActivity.class)); | ||||
|     } | ||||
|  | ||||
|     private void saveConfig() | ||||
|     { | ||||
|         if (mConfig == null) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             mConfig.save(new File(FileUtil.configFileLocation(Storage.getVcmiDataDir(this)))); | ||||
|         } | ||||
|         catch (final Exception e) | ||||
|         { | ||||
|             Toast.makeText(this, getString(R.string.launcher_error_config_saving_failed, e.getMessage()), Toast.LENGTH_LONG).show(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void loadConfigFile() | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             final String settingsFileContent = FileUtil.read( | ||||
|                     new File(FileUtil.configFileLocation(Storage.getVcmiDataDir(this)))); | ||||
|  | ||||
|             mConfig = Config.load(new JSONObject(settingsFileContent)); | ||||
|         } | ||||
|         catch (final Exception e) | ||||
|         { | ||||
|             Log.e(this, "Could not load config file", e); | ||||
|             mConfig = new Config(); | ||||
|         } | ||||
|         onConfigUpdated(); | ||||
|     } | ||||
|  | ||||
|     private void onConfigUpdated() | ||||
|     { | ||||
|         if(mConfig.mScreenScale == -1) | ||||
|             mConfig.updateScreenScale(ScreenScaleSettingDialog.getSupportedScalingRange(ActivityLauncher.this)[1]); | ||||
|  | ||||
|         updateCtrlConfig(mCtrlLanguage, mConfig); | ||||
|         updateCtrlConfig(mCtrlPointerMode, mConfig); | ||||
|         updateCtrlConfig(mCtrlPointerMulti, mConfig); | ||||
|         updateCtrlConfig(mCtrlScreenScale, mConfig); | ||||
|         updateCtrlConfig(mCtrlSoundVol, mConfig); | ||||
|         updateCtrlConfig(mCtrlMusicVol, mConfig); | ||||
|         updateCtrlConfig(mAiController, mConfig); | ||||
|     } | ||||
|  | ||||
|     private <TSetting, TConf> void updateCtrlConfig( | ||||
|             final LauncherSettingController<TSetting, TConf> ctrl, | ||||
|             final TConf config) | ||||
|     { | ||||
|         if (ctrl != null) | ||||
|         { | ||||
|             ctrl.updateConfig(config); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void onInitFailure(final AsyncLauncherInitialization.InitResult initResult) | ||||
|     { | ||||
|         Log.d(this, "Init failed with " + initResult); | ||||
|  | ||||
|         mProgress.setVisibility(View.GONE); | ||||
|         mCtrlStart.hide(); | ||||
|  | ||||
|         for (LauncherSettingController<?, ?> setting: mActualSettings) | ||||
|         { | ||||
|             setting.hide(); | ||||
|         } | ||||
|  | ||||
|         mErrorMessage.setVisibility(View.VISIBLE); | ||||
|         mErrorMessage.setText(initResult.mMessage); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,351 +0,0 @@ | ||||
| package eu.vcmi.vcmi; | ||||
|  | ||||
| import android.content.DialogInterface; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.Bundle; | ||||
|  | ||||
| import androidx.annotation.Nullable; | ||||
| import com.google.android.material.snackbar.Snackbar; | ||||
|  | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
| import androidx.recyclerview.widget.DefaultItemAnimator; | ||||
| import androidx.recyclerview.widget.DividerItemDecoration; | ||||
| import androidx.recyclerview.widget.LinearLayoutManager; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
|  | ||||
| import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| import android.view.MenuItem; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import com.google.android.gms.common.GooglePlayServicesNotAvailableException; | ||||
| import com.google.android.gms.common.GooglePlayServicesRepairableException; | ||||
| import com.google.android.gms.common.GooglePlayServicesUtil; | ||||
| import com.google.android.gms.security.ProviderInstaller; | ||||
|  | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
|  | ||||
| import eu.vcmi.vcmi.content.ModBaseViewHolder; | ||||
| import eu.vcmi.vcmi.content.ModsAdapter; | ||||
| import eu.vcmi.vcmi.mods.VCMIMod; | ||||
| import eu.vcmi.vcmi.mods.VCMIModContainer; | ||||
| import eu.vcmi.vcmi.mods.VCMIModsRepo; | ||||
| import eu.vcmi.vcmi.util.InstallModAsync; | ||||
| import eu.vcmi.vcmi.util.FileUtil; | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
| import eu.vcmi.vcmi.util.ServerResponse; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ActivityMods extends ActivityWithToolbar | ||||
| { | ||||
|     private static final boolean ENABLE_REPO_DOWNLOADING = true; | ||||
|     private static final String REPO_URL = "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.5.json"; | ||||
|     private VCMIModsRepo mRepo; | ||||
|     private RecyclerView mRecycler; | ||||
|  | ||||
|     private VCMIModContainer mModContainer; | ||||
|     private TextView mErrorMessage; | ||||
|     private View mProgress; | ||||
|     private ModsAdapter mModsAdapter; | ||||
|  | ||||
|     @Override | ||||
|     protected void onCreate(@Nullable final Bundle savedInstanceState) | ||||
|     { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_mods); | ||||
|         initToolbar(R.string.mods_title); | ||||
|  | ||||
|         mRepo = new VCMIModsRepo(); | ||||
|  | ||||
|         mProgress = findViewById(R.id.mods_progress); | ||||
|  | ||||
|         mErrorMessage = (TextView) findViewById(R.id.mods_error_text); | ||||
|         mErrorMessage.setVisibility(View.GONE); | ||||
|  | ||||
|         mRecycler = (RecyclerView) findViewById(R.id.mods_recycler); | ||||
|         mRecycler.setItemAnimator(new DefaultItemAnimator()); | ||||
|         mRecycler.setLayoutManager(new LinearLayoutManager(this)); | ||||
|         mRecycler.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); | ||||
|         mRecycler.setVisibility(View.GONE); | ||||
|  | ||||
|         mModsAdapter = new ModsAdapter(new OnAdapterItemAction()); | ||||
|         mRecycler.setAdapter(mModsAdapter); | ||||
|  | ||||
|         new AsyncLoadLocalMods().execute((Void) null); | ||||
|  | ||||
|         try { | ||||
|             ProviderInstaller.installIfNeeded(this); | ||||
|         } catch (GooglePlayServicesRepairableException e) { | ||||
|             GooglePlayServicesUtil.getErrorDialog(e.getConnectionStatusCode(), this, 0); | ||||
|         } catch (GooglePlayServicesNotAvailableException e) { | ||||
|             Log.e("SecurityException", "Google Play Services not available."); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void loadLocalModData() throws IOException, JSONException | ||||
|     { | ||||
|         final File dataRoot = Storage.getVcmiDataDir(this); | ||||
|         final String internalDataRoot = getFilesDir() + "/" + Const.VCMI_DATA_ROOT_FOLDER_NAME; | ||||
|  | ||||
|         final File modsRoot = new File(dataRoot,"/Mods"); | ||||
|         final File internalModsRoot = new File(internalDataRoot + "/Mods"); | ||||
|         if (!modsRoot.exists() && !internalModsRoot.exists()) | ||||
|         { | ||||
|             Log.w(this, "We don't have mods folders"); | ||||
|             return; | ||||
|         } | ||||
|         final File[] modsFiles = modsRoot.listFiles(); | ||||
|         final File[] internalModsFiles = internalModsRoot.listFiles(); | ||||
|         final List<File> topLevelModsFolders = new ArrayList<>(); | ||||
|         if (modsFiles != null && modsFiles.length > 0) | ||||
|         { | ||||
|             Collections.addAll(topLevelModsFolders, modsFiles); | ||||
|         } | ||||
|         if (internalModsFiles != null && internalModsFiles.length > 0) | ||||
|         { | ||||
|             Collections.addAll(topLevelModsFolders, internalModsFiles); | ||||
|         } | ||||
|         mModContainer = VCMIModContainer.createContainer(topLevelModsFolders); | ||||
|  | ||||
|         final File modConfigFile = new File(dataRoot, "config/modSettings.json"); | ||||
|         if (!modConfigFile.exists()) | ||||
|         { | ||||
|             Log.w(this, "We don't have mods config"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         JSONObject rootConfigObj = new JSONObject(FileUtil.read(modConfigFile)); | ||||
|         JSONObject activeMods = rootConfigObj.getJSONObject("activeMods"); | ||||
|         mModContainer.updateContainerFromConfigJson(activeMods, rootConfigObj.optJSONObject("core")); | ||||
|  | ||||
|         Log.i(this, "Loaded mods: " + mModContainer); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onCreateOptionsMenu(final Menu menu) | ||||
|     { | ||||
|         final MenuInflater menuInflater = getMenuInflater(); | ||||
|         menuInflater.inflate(R.menu.menu_mods, menu); | ||||
|         return super.onCreateOptionsMenu(menu); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(final MenuItem item) | ||||
|     { | ||||
|         if (item.getItemId() == R.id.menu_mods_download_repo) | ||||
|         { | ||||
|             Log.i(this, "Should download repo now..."); | ||||
|             if (ENABLE_REPO_DOWNLOADING) | ||||
|             { | ||||
|                 mProgress.setVisibility(View.VISIBLE); | ||||
|                 mRepo.init(REPO_URL, new OnModsRepoInitialized()); // disabled because the json is broken anyway | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 Snackbar.make(findViewById(R.id.mods_data_root), "Loading repo is disabled for now, because .json can't be parsed anyway", | ||||
|                     Snackbar.LENGTH_LONG).show(); | ||||
|             } | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item); | ||||
|     } | ||||
|  | ||||
|     private void handleNoData() | ||||
|     { | ||||
|         mProgress.setVisibility(View.GONE); | ||||
|         mRecycler.setVisibility(View.GONE); | ||||
|         mErrorMessage.setVisibility(View.VISIBLE); | ||||
|         mErrorMessage.setText("Could not load local mods list"); | ||||
|     } | ||||
|  | ||||
|     private void saveModSettingsToFile() | ||||
|     { | ||||
|         mModContainer.saveToFile( | ||||
|                 new File( | ||||
|                         Storage.getVcmiDataDir(this), | ||||
|                         "config/modSettings.json")); | ||||
|     } | ||||
|  | ||||
|     private class OnModsRepoInitialized implements VCMIModsRepo.IOnModsRepoDownloaded | ||||
|     { | ||||
|         @Override | ||||
|         public void onSuccess(ServerResponse<List<VCMIMod>> response) | ||||
|         { | ||||
|             Log.i(this, "Initialized mods repo"); | ||||
| 			if (mModContainer == null) | ||||
| 			{ | ||||
| 				handleNoData(); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				mModContainer.updateFromRepo(response.mContent); | ||||
| 				mModsAdapter.updateModsList(mModContainer.submods()); | ||||
| 				mProgress.setVisibility(View.GONE); | ||||
| 			} | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onError(final int code) | ||||
|         { | ||||
|             Log.i(this, "Mods repo error: " + code); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private class AsyncLoadLocalMods extends AsyncTask<Void, Void, Void> | ||||
|     { | ||||
|         @Override | ||||
|         protected void onPreExecute() | ||||
|         { | ||||
|             mProgress.setVisibility(View.VISIBLE); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected Void doInBackground(final Void... params) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 loadLocalModData(); | ||||
|             } | ||||
|             catch (IOException e) | ||||
|             { | ||||
|                 Log.e(this, "Loading local mod data failed", e); | ||||
|             } | ||||
|             catch (JSONException e) | ||||
|             { | ||||
|                 Log.e(this, "Parsing local mod data failed", e); | ||||
|             } | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void onPostExecute(final Void aVoid) | ||||
|         { | ||||
|             if (mModContainer == null || !mModContainer.hasSubmods()) | ||||
|             { | ||||
|                 handleNoData(); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 mProgress.setVisibility(View.GONE); | ||||
|                 mRecycler.setVisibility(View.VISIBLE); | ||||
|                 mModsAdapter.updateModsList(mModContainer.submods()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private class OnAdapterItemAction implements ModsAdapter.IOnItemAction | ||||
|     { | ||||
|         @Override | ||||
|         public void onItemPressed(final ModsAdapter.ModItem mod, final RecyclerView.ViewHolder vh) | ||||
|         { | ||||
|             Log.i(this, "Mod pressed: " + mod); | ||||
|             if (mod.mMod.hasSubmods()) | ||||
|             { | ||||
|                 if (mod.mExpanded) | ||||
|                 { | ||||
|                     mModsAdapter.detachSubmods(mod, vh); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     mModsAdapter.attachSubmods(mod, vh); | ||||
|                     mRecycler.scrollToPosition(vh.getAdapterPosition() + 1); | ||||
|                 } | ||||
|                 mod.mExpanded = !mod.mExpanded; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onDownloadPressed(final ModsAdapter.ModItem mod, final RecyclerView.ViewHolder vh) | ||||
|         { | ||||
|             Log.i(this, "Mod download pressed: " + mod); | ||||
|             mModsAdapter.downloadProgress(mod, "0%"); | ||||
|             installModAsync(mod); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onTogglePressed(final ModsAdapter.ModItem item, final ModBaseViewHolder holder) | ||||
|         { | ||||
|             if(!item.mMod.mSystem && item.mMod.mInstalled) | ||||
|             { | ||||
|                 item.mMod.mActive = !item.mMod.mActive; | ||||
|                 mModsAdapter.notifyItemChanged(holder.getAdapterPosition()); | ||||
|                 saveModSettingsToFile(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onUninstall(ModsAdapter.ModItem item, ModBaseViewHolder holder) | ||||
|         { | ||||
|             File installationFolder = item.mMod.installationFolder; | ||||
|             ActivityMods activity = ActivityMods.this; | ||||
|  | ||||
|             if(installationFolder != null){ | ||||
|                 new AlertDialog.Builder(activity) | ||||
|                     .setTitle(activity.getString(R.string.mods_removal_title, item.mMod.mName)) | ||||
|                     .setMessage(activity.getString(R.string.mods_removal_confirmation, item.mMod.mName)) | ||||
|                     .setIcon(android.R.drawable.ic_dialog_alert) | ||||
|                     .setNegativeButton(android.R.string.no, null) | ||||
|                     .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> | ||||
|                     { | ||||
|                         FileUtil.clearDirectory(installationFolder); | ||||
|                         installationFolder.delete(); | ||||
|  | ||||
|                         mModsAdapter.modRemoved(item); | ||||
|                     }) | ||||
|                     .show(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void installModAsync(ModsAdapter.ModItem mod){ | ||||
|         File dataDir = Storage.getVcmiDataDir(this); | ||||
|         File modFolder = new File( | ||||
|                 new File(dataDir, "Mods"), | ||||
|                 mod.mMod.mId.toLowerCase(Locale.US)); | ||||
|  | ||||
|         InstallModAsync modInstaller = new InstallModAsync( | ||||
|             modFolder, | ||||
|             this, | ||||
|             new InstallModCallback(mod) | ||||
|         ); | ||||
|  | ||||
|         modInstaller.execute(mod.mMod.mArchiveUrl); | ||||
|     } | ||||
|  | ||||
|     public class InstallModCallback implements InstallModAsync.PostDownload | ||||
|     { | ||||
|         private ModsAdapter.ModItem mod; | ||||
|  | ||||
|         public InstallModCallback(ModsAdapter.ModItem mod) | ||||
|         { | ||||
|             this.mod = mod; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void downloadDone(Boolean succeed, File modFolder) | ||||
|         { | ||||
|             if(succeed){ | ||||
|                 mModsAdapter.modInstalled(mod, modFolder); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void downloadProgress(String... progress) | ||||
|         { | ||||
|             if(progress.length > 0) | ||||
|             { | ||||
|                 mModsAdapter.downloadProgress(mod, progress[0]); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,53 +0,0 @@ | ||||
| package eu.vcmi.vcmi; | ||||
|  | ||||
| import androidx.appcompat.app.ActionBar; | ||||
| import androidx.appcompat.widget.Toolbar; | ||||
| import android.view.MenuItem; | ||||
| import android.view.ViewStub; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public abstract class ActivityWithToolbar extends ActivityBase | ||||
| { | ||||
|     @Override | ||||
|     public void setContentView(final int layoutResId) | ||||
|     { | ||||
|         super.setContentView(R.layout.activity_toolbar_wrapper); | ||||
|         final ViewStub contentStub = (ViewStub) findViewById(R.id.toolbar_wrapper_content_stub); | ||||
|         contentStub.setLayoutResource(layoutResId); | ||||
|         contentStub.inflate(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(final MenuItem item) | ||||
|     { | ||||
|         if (item.getItemId() == android.R.id.home) | ||||
|         { | ||||
|             finish(); | ||||
|             return true; | ||||
|         } | ||||
|         return super.onOptionsItemSelected(item); | ||||
|     } | ||||
|  | ||||
|     protected void initToolbar(final int textResId) | ||||
|     { | ||||
|         initToolbar(textResId, false); | ||||
|     } | ||||
|  | ||||
|     protected void initToolbar(final int textResId, final boolean isTopLevelActivity) | ||||
|     { | ||||
|         final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); | ||||
|         setSupportActionBar(toolbar); | ||||
|         toolbar.setTitle(textResId); | ||||
|  | ||||
|         if (!isTopLevelActivity) | ||||
|         { | ||||
|             final ActionBar bar = getSupportActionBar(); | ||||
|             if (bar != null) | ||||
|             { | ||||
|                 bar.setDisplayHomeAsUpEnabled(true); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,217 +0,0 @@ | ||||
| package eu.vcmi.vcmi; | ||||
|  | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
|  | ||||
| import eu.vcmi.vcmi.util.FileUtil; | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class Config | ||||
| { | ||||
|     public static final String DEFAULT_LANGUAGE = "english"; | ||||
|     public static final int DEFAULT_MUSIC_VALUE = 5; | ||||
|     public static final int DEFAULT_SOUND_VALUE = 5; | ||||
|  | ||||
|     public String mLanguage; | ||||
|     public int mScreenScale; | ||||
|     public int mVolumeSound; | ||||
|     public int mVolumeMusic; | ||||
|     private String adventureAi; | ||||
|     private double mPointerSpeedMultiplier; | ||||
|     private boolean mUseRelativePointer; | ||||
|     private JSONObject mRawObject; | ||||
|  | ||||
|     private boolean mIsModified; | ||||
|  | ||||
|     private static JSONObject accessNode(final JSONObject baseObj, String type) | ||||
|     { | ||||
|         if (baseObj == null) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         return baseObj.optJSONObject(type); | ||||
|     } | ||||
|  | ||||
|     private static JSONObject accessResolutionNode(final JSONObject baseObj) | ||||
|     { | ||||
|         if (baseObj == null) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         final JSONObject video = baseObj.optJSONObject("video"); | ||||
|         if (video != null) | ||||
|         { | ||||
|             return video.optJSONObject("resolution"); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     private static double loadDouble(final JSONObject node, final String key, final double fallback) | ||||
|     { | ||||
|         if (node == null) | ||||
|         { | ||||
|             return fallback; | ||||
|         } | ||||
|  | ||||
|         return node.optDouble(key, fallback); | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unchecked") | ||||
|     private static <T> T loadEntry(final JSONObject node, final String key, final T fallback) | ||||
|     { | ||||
|         if (node == null) | ||||
|         { | ||||
|             return fallback; | ||||
|         } | ||||
|         final Object value = node.opt(key); | ||||
|         return value == null ? fallback : (T) value; | ||||
|     } | ||||
|  | ||||
|     public static Config load(final JSONObject obj) | ||||
|     { | ||||
|         Log.v("loading config from json: " + obj.toString()); | ||||
|         final Config config = new Config(); | ||||
|         final JSONObject general = accessNode(obj, "general"); | ||||
|         final JSONObject server = accessNode(obj, "server"); | ||||
|         final JSONObject resolution = accessResolutionNode(obj); | ||||
|         config.mLanguage = loadEntry(general, "language", DEFAULT_LANGUAGE); | ||||
|         config.mScreenScale = loadEntry(resolution, "scaling", -1); | ||||
|         config.mVolumeSound = loadEntry(general, "sound", DEFAULT_SOUND_VALUE); | ||||
|         config.mVolumeMusic = loadEntry(general, "music", DEFAULT_MUSIC_VALUE); | ||||
|         config.adventureAi = loadEntry(server, "playerAI", "Nullkiller"); | ||||
|         config.mUseRelativePointer = loadEntry(general, "userRelativePointer", false); | ||||
|         config.mPointerSpeedMultiplier = loadDouble(general, "relativePointerSpeedMultiplier", 1.0); | ||||
|  | ||||
|         config.mRawObject = obj; | ||||
|         return config; | ||||
|     } | ||||
|  | ||||
|     public void updateLanguage(final String s) | ||||
|     { | ||||
|         mLanguage = s; | ||||
|         mIsModified = true; | ||||
|     } | ||||
|  | ||||
|     public void updateScreenScale(final int scale) | ||||
|     { | ||||
|         mScreenScale = scale; | ||||
|         mIsModified = true; | ||||
|     } | ||||
|  | ||||
|     public void updateSound(final int i) | ||||
|     { | ||||
|         mVolumeSound = i; | ||||
|         mIsModified = true; | ||||
|     } | ||||
|  | ||||
|     public void updateMusic(final int i) | ||||
|     { | ||||
|         mVolumeMusic = i; | ||||
|         mIsModified = true; | ||||
|     } | ||||
|  | ||||
|     public void setAdventureAi(String ai) | ||||
|     { | ||||
|         adventureAi = ai; | ||||
|         mIsModified = true; | ||||
|     } | ||||
|  | ||||
|     public String getAdventureAi() | ||||
|     { | ||||
|         return this.adventureAi == null ? "Nullkiller" : this.adventureAi; | ||||
|     } | ||||
|  | ||||
|     public void setPointerSpeedMultiplier(float speedMultiplier) | ||||
|     { | ||||
|         mPointerSpeedMultiplier = speedMultiplier; | ||||
|         mIsModified = true; | ||||
|     } | ||||
|  | ||||
|     public float getPointerSpeedMultiplier() | ||||
|     { | ||||
|         return (float)mPointerSpeedMultiplier; | ||||
|     } | ||||
|  | ||||
|     public void setPointerMode(boolean isRelative) | ||||
|     { | ||||
|         mUseRelativePointer = isRelative; | ||||
|         mIsModified = true; | ||||
|     } | ||||
|  | ||||
|     public boolean getPointerModeIsRelative() | ||||
|     { | ||||
|         return mUseRelativePointer; | ||||
|     } | ||||
|  | ||||
|     public void save(final File location) throws IOException, JSONException | ||||
|     { | ||||
|         if (!needsSaving(location)) | ||||
|         { | ||||
|             Log.d(this, "Config doesn't need saving"); | ||||
|             return; | ||||
|         } | ||||
|         try | ||||
|         { | ||||
|             final String configString = toJson(); | ||||
|             FileUtil.write(location, configString); | ||||
|             Log.v(this, "Saved config: " + configString); | ||||
|         } | ||||
|         catch (final Exception e) | ||||
|         { | ||||
|             Log.e(this, "Could not save config", e); | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private boolean needsSaving(final File location) | ||||
|     { | ||||
|         return mIsModified || !location.exists(); | ||||
|     } | ||||
|  | ||||
|     private String toJson() throws JSONException | ||||
|     { | ||||
|         final JSONObject generalNode = accessNode(mRawObject, "general"); | ||||
|         final JSONObject serverNode = accessNode(mRawObject, "server"); | ||||
|         final JSONObject resolutionNode = accessResolutionNode(mRawObject); | ||||
|  | ||||
|         final JSONObject root = mRawObject == null ? new JSONObject() : mRawObject; | ||||
|         final JSONObject general = generalNode == null ? new JSONObject() : generalNode; | ||||
|         final JSONObject video = new JSONObject(); | ||||
|         final JSONObject resolution = resolutionNode == null ? new JSONObject() : resolutionNode; | ||||
|         final JSONObject server = serverNode == null ? new JSONObject() : serverNode; | ||||
|  | ||||
|         if (mLanguage != null) | ||||
|         { | ||||
|             general.put("language", mLanguage); | ||||
|         } | ||||
|  | ||||
|         general.put("music", mVolumeMusic); | ||||
|         general.put("sound", mVolumeSound); | ||||
|         general.put("userRelativePointer", mUseRelativePointer); | ||||
|         general.put("relativePointerSpeedMultiplier", mPointerSpeedMultiplier); | ||||
|         root.put("general", general); | ||||
|  | ||||
|         if(this.adventureAi != null) | ||||
|         { | ||||
|             server.put("playerAI", this.adventureAi); | ||||
|             root.put("server", server); | ||||
|         } | ||||
|  | ||||
|         if (mScreenScale > 0) | ||||
|         { | ||||
|             resolution.put("scaling", mScreenScale); | ||||
|             video.put("resolution", resolution); | ||||
|             root.put("video", video); | ||||
|         } | ||||
|  | ||||
|         return root.toString(); | ||||
|     } | ||||
| } | ||||
| @@ -1,15 +1,6 @@ | ||||
| package eu.vcmi.vcmi; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.os.Build; | ||||
| import android.os.Environment; | ||||
|  | ||||
| import java.io.BufferedWriter; | ||||
| import java.io.File; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.io.OutputStreamWriter; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
| @@ -17,8 +8,6 @@ import java.io.OutputStreamWriter; | ||||
| public class Const | ||||
| { | ||||
|     public static final String JNI_METHOD_SUPPRESS = "unused"; // jni methods are marked as unused, because IDE doesn't understand jni calls | ||||
|     // used to disable lint errors about try-with-resources being unsupported on api <19 (it is supported, because retrolambda backports it) | ||||
|     public static final int SUPPRESS_TRY_WITH_RESOURCES_WARNING = Build.VERSION_CODES.KITKAT; | ||||
|  | ||||
|     public static final String VCMI_DATA_ROOT_FOLDER_NAME = "vcmi-data"; | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,8 @@ | ||||
| package eu.vcmi.vcmi; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.os.Build; | ||||
| import android.os.Environment; | ||||
| import android.os.Looper; | ||||
| import android.os.Message; | ||||
| import android.os.Messenger; | ||||
| import android.os.RemoteException; | ||||
| import android.os.VibrationEffect; | ||||
| import android.os.Vibrator; | ||||
|  | ||||
| @@ -17,9 +11,6 @@ import org.libsdl.app.SDLActivity; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.util.Date; | ||||
| import java.util.Locale; | ||||
| import java.text.SimpleDateFormat; | ||||
|  | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
|  | ||||
| @@ -35,7 +26,7 @@ public class NativeMethods | ||||
|     } | ||||
|  | ||||
|     public static native void initClassloader(); | ||||
|  | ||||
|     public static native void heroesDataUpdate(); | ||||
|     public static native boolean tryToSaveTheGame(); | ||||
|  | ||||
|     public static void setupMsg(final Messenger msg) | ||||
|   | ||||
| @@ -1,33 +1,14 @@ | ||||
| package eu.vcmi.vcmi; | ||||
|  | ||||
| import android.content.Context; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import eu.vcmi.vcmi.util.FileUtil; | ||||
|  | ||||
| public class Storage | ||||
| { | ||||
|     public static File getVcmiDataDir(Context context) | ||||
|     { | ||||
|         File root = context.getExternalFilesDir(null); | ||||
|  | ||||
|         return new File(root, Const.VCMI_DATA_ROOT_FOLDER_NAME); | ||||
|     } | ||||
|  | ||||
|     public static boolean testH3DataFolder(Context context) | ||||
|     { | ||||
|         return testH3DataFolder(getVcmiDataDir(context)); | ||||
|     } | ||||
|  | ||||
|     public static boolean testH3DataFolder(final File baseDir) | ||||
|     { | ||||
|         final File testH3Data = new File(baseDir, "Data"); | ||||
|         final File testH3data = new File(baseDir, "data"); | ||||
|         final File testH3DATA = new File(baseDir, "DATA"); | ||||
|         return testH3Data.exists() || testH3data.exists() || testH3DATA.exists(); | ||||
|     } | ||||
|  | ||||
|     public static String getH3DataFolder(Context context){ | ||||
|         return getVcmiDataDir(context).getAbsolutePath(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -84,9 +84,7 @@ public class VcmiSDLActivity extends SDLActivity | ||||
|  | ||||
|     @Override | ||||
|     protected String getMainSharedObject() { | ||||
|         String library = "libvcmiclient.so"; | ||||
|  | ||||
|         return getContext().getApplicationInfo().nativeLibraryDir + "/" + library; | ||||
|         return String.format("%s/lib%s.so", getContext().getApplicationInfo().nativeLibraryDir, LibsLoader.CLIENT_LIB); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -100,9 +98,6 @@ public class VcmiSDLActivity extends SDLActivity | ||||
|     { | ||||
|         super.onCreate(savedInstanceState); | ||||
|  | ||||
|         if(mBrokenLibraries) | ||||
|             return; | ||||
|  | ||||
|         final View outerLayout = getLayoutInflater().inflate(R.layout.activity_game, null, false); | ||||
|         final ViewGroup layout = (ViewGroup) outerLayout.findViewById(R.id.game_outer_frame); | ||||
|         mProgressBar = outerLayout.findViewById(R.id.game_progress); | ||||
| @@ -182,4 +177,4 @@ public class VcmiSDLActivity extends SDLActivity | ||||
|             mCallback = callback; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,171 +0,0 @@ | ||||
| package eu.vcmi.vcmi.content; | ||||
|  | ||||
| import android.Manifest; | ||||
| import android.app.Activity; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.net.Uri; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.Build; | ||||
| import android.os.Environment; | ||||
| import androidx.core.app.ActivityCompat; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import eu.vcmi.vcmi.Const; | ||||
| import eu.vcmi.vcmi.R; | ||||
| import eu.vcmi.vcmi.Storage; | ||||
| import eu.vcmi.vcmi.util.FileUtil; | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
| import eu.vcmi.vcmi.util.SharedPrefs; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class AsyncLauncherInitialization extends AsyncTask<Void, Void, AsyncLauncherInitialization.InitResult> | ||||
| { | ||||
|     private final WeakReference<ILauncherCallbacks> mCallbackRef; | ||||
|  | ||||
|     public AsyncLauncherInitialization(final ILauncherCallbacks callback) | ||||
|     { | ||||
|         mCallbackRef = new WeakReference<>(callback); | ||||
|     } | ||||
|  | ||||
|     private InitResult init() | ||||
|     { | ||||
|         InitResult initResult = handleDataFoldersInitialization(); | ||||
|  | ||||
|         if (!initResult.mSuccess) | ||||
|         { | ||||
|             return initResult; | ||||
|         } | ||||
|         Log.d(this, "Folders check passed"); | ||||
|  | ||||
|         return initResult; | ||||
|     } | ||||
|  | ||||
|     private InitResult handleDataFoldersInitialization() | ||||
|     { | ||||
|         final ILauncherCallbacks callbacks = mCallbackRef.get(); | ||||
|  | ||||
|         if (callbacks == null) | ||||
|         { | ||||
|             return InitResult.failure("Internal error"); | ||||
|         } | ||||
|  | ||||
|         final Context ctx = callbacks.ctx(); | ||||
|         final File vcmiDir = Storage.getVcmiDataDir(ctx); | ||||
|  | ||||
|         final File internalDir = ctx.getFilesDir(); | ||||
|         final File vcmiInternalDir = new File(internalDir, Const.VCMI_DATA_ROOT_FOLDER_NAME); | ||||
|         Log.i(this, "Using " + vcmiDir.getAbsolutePath() + " as root vcmi dir"); | ||||
|  | ||||
|         if(!vcmiInternalDir.exists()) vcmiInternalDir.mkdir(); | ||||
|         if(!vcmiDir.exists()) vcmiDir.mkdir(); | ||||
|  | ||||
|         if (!Storage.testH3DataFolder(ctx)) | ||||
|         { | ||||
|             // no h3 data present -> instruct user where to put it | ||||
|             return InitResult.failure( | ||||
|                 ctx.getString( | ||||
|                     R.string.launcher_error_h3_data_missing, | ||||
|                         Storage.getVcmiDataDir(ctx))); | ||||
|         } | ||||
|  | ||||
|         final File testVcmiData = new File(vcmiInternalDir, "Mods/vcmi/mod.json"); | ||||
|         final boolean internalVcmiDataExisted = testVcmiData.exists(); | ||||
|         if (!internalVcmiDataExisted && !FileUtil.unpackVcmiDataToInternalDir(vcmiInternalDir, ctx.getAssets())) | ||||
|         { | ||||
|             // no h3 data present -> instruct user where to put it | ||||
|             return InitResult.failure(ctx.getString(R.string.launcher_error_vcmi_data_internal_missing)); | ||||
|         } | ||||
|  | ||||
|         final String previousInternalDataHash = callbacks.prefs().load(SharedPrefs.KEY_CURRENT_INTERNAL_ASSET_HASH, null); | ||||
|         final String currentInternalDataHash = FileUtil.readAssetsStream(ctx.getAssets(), "internalDataHash.txt"); | ||||
|         if (currentInternalDataHash == null || previousInternalDataHash == null || !currentInternalDataHash.equals(previousInternalDataHash)) | ||||
|         { | ||||
|             // we should update the data only if it existed previously (hash is bound to be empty if we have just created the data) | ||||
|             if (internalVcmiDataExisted) | ||||
|             { | ||||
|                 Log.i(this, "Internal data needs to be created/updated; old hash=" + previousInternalDataHash | ||||
|                             + ", new hash=" + currentInternalDataHash); | ||||
|                 if (!FileUtil.reloadVcmiDataToInternalDir(vcmiInternalDir, ctx.getAssets())) | ||||
|                 { | ||||
|                     return InitResult.failure(ctx.getString(R.string.launcher_error_vcmi_data_internal_update)); | ||||
|                 } | ||||
|             } | ||||
|             callbacks.prefs().save(SharedPrefs.KEY_CURRENT_INTERNAL_ASSET_HASH, currentInternalDataHash); | ||||
|         } | ||||
|  | ||||
|         return InitResult.success(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected InitResult doInBackground(final Void... params) | ||||
|     { | ||||
|         return init(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onPostExecute(final InitResult initResult) | ||||
|     { | ||||
|         final ILauncherCallbacks callbacks = mCallbackRef.get(); | ||||
|         if (callbacks == null) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (initResult.mSuccess) | ||||
|         { | ||||
|             callbacks.onInitSuccess(); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             callbacks.onInitFailure(initResult); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public interface ILauncherCallbacks | ||||
|     { | ||||
|         Activity ctx(); | ||||
|  | ||||
|         SharedPrefs prefs(); | ||||
|  | ||||
|         void onInitSuccess(); | ||||
|  | ||||
|         void onInitFailure(InitResult result); | ||||
|     } | ||||
|  | ||||
|     public static final class InitResult | ||||
|     { | ||||
|         public final boolean mSuccess; | ||||
|         public final String mMessage; | ||||
|         public boolean mFailSilently; | ||||
|  | ||||
|         public InitResult(final boolean success, final String message) | ||||
|         { | ||||
|             mSuccess = success; | ||||
|             mMessage = message; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String toString() | ||||
|         { | ||||
|             return String.format("success: %s (%s)", mSuccess, mMessage); | ||||
|         } | ||||
|  | ||||
|         public static InitResult failure(String message) | ||||
|         { | ||||
|             return new InitResult(false, message); | ||||
|         } | ||||
|  | ||||
|         public static InitResult success() | ||||
|         { | ||||
|             return new InitResult(true, ""); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,52 +0,0 @@ | ||||
| package eu.vcmi.vcmi.content; | ||||
|  | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.Dialog; | ||||
| import android.os.Bundle; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.fragment.app.DialogFragment; | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import java.io.IOException; | ||||
|  | ||||
| import eu.vcmi.vcmi.R; | ||||
| import eu.vcmi.vcmi.util.FileUtil; | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class DialogAuthors extends DialogFragment | ||||
| { | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Dialog onCreateDialog(final Bundle savedInstanceState) | ||||
|     { | ||||
|         final LayoutInflater inflater = LayoutInflater.from(getActivity()); | ||||
|         @SuppressLint("InflateParams") final View inflated = inflater.inflate(R.layout.dialog_authors, null, false); | ||||
|         final TextView vcmiAuthorsView = (TextView) inflated.findViewById(R.id.dialog_authors_vcmi); | ||||
|         final TextView launcherAuthorsView = (TextView) inflated.findViewById(R.id.dialog_authors_launcher); | ||||
|         loadAuthorsContent(vcmiAuthorsView, launcherAuthorsView); | ||||
|         return new AlertDialog.Builder(getActivity()) | ||||
|             .setView(inflated) | ||||
|             .create(); | ||||
|     } | ||||
|  | ||||
|     private void loadAuthorsContent(final TextView vcmiAuthorsView, final TextView launcherAuthorsView) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             // to be checked if this should be converted to async load (not really a file operation so it should be okay) | ||||
|             final String authorsContent = "See ingame credits"; | ||||
|             vcmiAuthorsView.setText(authorsContent); | ||||
|             launcherAuthorsView.setText("Fay"); // TODO hardcoded for now | ||||
|         } | ||||
|         catch (final Exception e) | ||||
|         { | ||||
|             Log.e(this, "Could not load authors content", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,36 +0,0 @@ | ||||
| package eu.vcmi.vcmi.content; | ||||
|  | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ModBaseViewHolder extends RecyclerView.ViewHolder | ||||
| { | ||||
|     final View mModNesting; | ||||
|     final TextView mModName; | ||||
|  | ||||
|     ModBaseViewHolder(final View parentView) | ||||
|     { | ||||
|         this( | ||||
|             LayoutInflater.from(parentView.getContext()).inflate( | ||||
|                 R.layout.mod_base_adapter_item, | ||||
|                 (ViewGroup) parentView, | ||||
|                 false), | ||||
|             true); | ||||
|     } | ||||
|  | ||||
|     protected ModBaseViewHolder(final View v, final boolean internal) | ||||
|     { | ||||
|         super(v); | ||||
|         mModNesting = itemView.findViewById(R.id.mods_adapter_item_nesting); | ||||
|         mModName = (TextView) itemView.findViewById(R.id.mods_adapter_item_name); | ||||
|     } | ||||
| } | ||||
| @@ -1,254 +0,0 @@ | ||||
| package eu.vcmi.vcmi.content; | ||||
|  | ||||
| import android.content.Context; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import java.io.File; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import eu.vcmi.vcmi.R; | ||||
| import eu.vcmi.vcmi.mods.VCMIMod; | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ModsAdapter extends RecyclerView.Adapter<ModBaseViewHolder> | ||||
| { | ||||
|     private static final int NESTING_WIDTH_PER_LEVEL = 16; | ||||
|     private static final int VIEWTYPE_MOD = 0; | ||||
|     private static final int VIEWTYPE_FAILED_MOD = 1; | ||||
|     private final List<ModItem> mDataset = new ArrayList<>(); | ||||
|     private final IOnItemAction mItemListener; | ||||
|  | ||||
|     public ModsAdapter(final IOnItemAction itemListener) | ||||
|     { | ||||
|         mItemListener = itemListener; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public ModBaseViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) | ||||
|     { | ||||
|         switch (viewType) | ||||
|         { | ||||
|             case VIEWTYPE_MOD: | ||||
|                 return new ModsViewHolder(parent); | ||||
|             case VIEWTYPE_FAILED_MOD: | ||||
|                 return new ModBaseViewHolder(parent); | ||||
|             default: | ||||
|                 Log.e(this, "Unhandled view type: " + viewType); | ||||
|                 return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBindViewHolder(final ModBaseViewHolder holder, final int position) | ||||
|     { | ||||
|         final ModItem item = mDataset.get(position); | ||||
|         final int viewType = getItemViewType(position); | ||||
|  | ||||
|         final Context ctx = holder.itemView.getContext(); | ||||
|         holder.mModNesting.getLayoutParams().width = item.mNestingLevel * NESTING_WIDTH_PER_LEVEL; | ||||
|         switch (viewType) | ||||
|         { | ||||
|             case VIEWTYPE_MOD: | ||||
|                 final ModsViewHolder modHolder = (ModsViewHolder) holder; | ||||
|                 modHolder.mModName.setText(item.mMod.mName + ", " + item.mMod.mVersion); | ||||
|                 modHolder.mModType.setText(item.mMod.mModType); | ||||
|                 if (item.mMod.mSize > 0) | ||||
|                 { | ||||
|                     modHolder.mModSize.setVisibility(View.VISIBLE); | ||||
|                     // TODO unit conversion | ||||
|                     modHolder.mModSize.setText(String.format(Locale.getDefault(), "%.1f kB", item.mMod.mSize / 1024.0f)); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     modHolder.mModSize.setVisibility(View.GONE); | ||||
|                 } | ||||
|                 modHolder.mModAuthor.setText(ctx.getString(R.string.mods_item_author_template, item.mMod.mAuthor)); | ||||
|                 modHolder.mStatusIcon.setImageResource(selectModStatusIcon(item.mMod.mActive)); | ||||
|  | ||||
|                 modHolder.mDownloadBtn.setVisibility(View.GONE); | ||||
|                 modHolder.mDownloadProgress.setVisibility(View.GONE); | ||||
|                 modHolder.mUninstall.setVisibility(View.GONE); | ||||
|  | ||||
|                 if(!item.mMod.mSystem) | ||||
|                 { | ||||
|                     if (item.mDownloadProgress != null) | ||||
|                     { | ||||
|                         modHolder.mDownloadProgress.setText(item.mDownloadProgress); | ||||
|                         modHolder.mDownloadProgress.setVisibility(View.VISIBLE); | ||||
|                     } | ||||
|                     else if (!item.mMod.mInstalled) | ||||
|                     { | ||||
|                         modHolder.mDownloadBtn.setVisibility(View.VISIBLE); | ||||
|                     } | ||||
|                     else if (item.mMod.installationFolder != null) | ||||
|                     { | ||||
|                         modHolder.mUninstall.setVisibility(View.VISIBLE); | ||||
|                     } | ||||
|  | ||||
|                     modHolder.itemView.setOnClickListener(v -> mItemListener.onItemPressed(item, holder)); | ||||
|                     modHolder.mStatusIcon.setOnClickListener(v -> mItemListener.onTogglePressed(item, holder)); | ||||
|                     modHolder.mDownloadBtn.setOnClickListener(v -> mItemListener.onDownloadPressed(item, holder)); | ||||
|                     modHolder.mUninstall.setOnClickListener(v -> mItemListener.onUninstall(item, holder)); | ||||
|                 } | ||||
|  | ||||
|                 break; | ||||
|             case VIEWTYPE_FAILED_MOD: | ||||
|                 holder.mModName.setText(ctx.getString(R.string.mods_failed_mod_loading, item.mMod.mName)); | ||||
|                 break; | ||||
|             default: | ||||
|                 Log.e(this, "Unhandled view type: " + viewType); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private int selectModStatusIcon(final boolean active) | ||||
|     { | ||||
|         // TODO distinguishing mods that aren't downloaded or have an update available | ||||
|         if (active) | ||||
|         { | ||||
|             return R.drawable.ic_star_full; | ||||
|         } | ||||
|         return R.drawable.ic_star_empty; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int getItemViewType(final int position) | ||||
|     { | ||||
|         return mDataset.get(position).mMod.mLoadedCorrectly ? VIEWTYPE_MOD : VIEWTYPE_FAILED_MOD; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public int getItemCount() | ||||
|     { | ||||
|         return mDataset.size(); | ||||
|     } | ||||
|  | ||||
|     public void attachSubmods(final ModItem mod, final RecyclerView.ViewHolder vh) | ||||
|     { | ||||
|         int adapterPosition = vh.getAdapterPosition(); | ||||
|         final List<ModItem> submods = new ArrayList<>(); | ||||
|  | ||||
|         for (VCMIMod v : mod.mMod.submods()) | ||||
|         { | ||||
|             ModItem modItem = new ModItem(v, mod.mNestingLevel + 1); | ||||
|             submods.add(modItem); | ||||
|         } | ||||
|  | ||||
|         mDataset.addAll(adapterPosition + 1, submods); | ||||
|         notifyItemRangeInserted(adapterPosition + 1, submods.size()); | ||||
|     } | ||||
|  | ||||
|     public void detachSubmods(final ModItem mod, final RecyclerView.ViewHolder vh) | ||||
|     { | ||||
|         final int adapterPosition = vh.getAdapterPosition(); | ||||
|         final int checkedPosition = adapterPosition + 1; | ||||
|         int detachedElements = 0; | ||||
|         while (checkedPosition < mDataset.size() && mDataset.get(checkedPosition).mNestingLevel > mod.mNestingLevel) | ||||
|         { | ||||
|             ++detachedElements; | ||||
|             mDataset.remove(checkedPosition); | ||||
|         } | ||||
|         notifyItemRangeRemoved(checkedPosition, detachedElements); | ||||
|     } | ||||
|  | ||||
|     public void updateModsList(List<VCMIMod> mods) | ||||
|     { | ||||
|         mDataset.clear(); | ||||
|  | ||||
|         List<ModItem> list = new ArrayList<>(); | ||||
|  | ||||
|         for (VCMIMod mod : mods) | ||||
|         { | ||||
|             ModItem modItem = new ModItem(mod); | ||||
|             list.add(modItem); | ||||
|         } | ||||
|  | ||||
|         mDataset.addAll(list); | ||||
|  | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public void modInstalled(ModItem mod, File modFolder) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             mod.mMod.updateFromModInfo(modFolder); | ||||
|             mod.mMod.mLoadedCorrectly = true; | ||||
|             mod.mMod.mActive = true; // active by default | ||||
|             mod.mMod.mInstalled = true; | ||||
|             mod.mMod.installationFolder = modFolder; | ||||
|             mod.mDownloadProgress = null; | ||||
|             notifyItemChanged(mDataset.indexOf(mod)); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             Log.e("Failed to install mod", ex); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void downloadProgress(ModItem mod, String progress) | ||||
|     { | ||||
|         mod.mDownloadProgress = progress; | ||||
|         notifyItemChanged(mDataset.indexOf(mod)); | ||||
|     } | ||||
|  | ||||
|     public void modRemoved(ModItem item) | ||||
|     { | ||||
|         int itemIndex = mDataset.indexOf(item); | ||||
|  | ||||
|         if(item.mMod.mArchiveUrl != null && item.mMod.mArchiveUrl != "") | ||||
|         { | ||||
|             item.mMod.mInstalled = false; | ||||
|             item.mMod.installationFolder = null; | ||||
|  | ||||
|             notifyItemChanged(itemIndex); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             mDataset.remove(item); | ||||
|             notifyItemRemoved(itemIndex); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public interface IOnItemAction | ||||
|     { | ||||
|         void onItemPressed(final ModItem mod, final RecyclerView.ViewHolder vh); | ||||
|  | ||||
|         void onDownloadPressed(final ModItem mod, final RecyclerView.ViewHolder vh); | ||||
|  | ||||
|         void onTogglePressed(ModItem item, ModBaseViewHolder holder); | ||||
|  | ||||
|         void onUninstall(ModItem item, ModBaseViewHolder holder); | ||||
|     } | ||||
|  | ||||
|     public static class ModItem | ||||
|     { | ||||
|         public final VCMIMod mMod; | ||||
|         public int mNestingLevel; | ||||
|         public boolean mExpanded; | ||||
|         public String mDownloadProgress; | ||||
|  | ||||
|         public ModItem(final VCMIMod mod) | ||||
|         { | ||||
|             this(mod, 0); | ||||
|         } | ||||
|  | ||||
|         public ModItem(final VCMIMod mod, final int nestingLevel) | ||||
|         { | ||||
|             mMod = mod; | ||||
|             mNestingLevel = nestingLevel; | ||||
|             mExpanded = false; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String toString() | ||||
|         { | ||||
|             return mMod.toString(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,35 +0,0 @@ | ||||
| package eu.vcmi.vcmi.content; | ||||
|  | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ModsViewHolder extends ModBaseViewHolder | ||||
| { | ||||
|     final TextView mModAuthor; | ||||
|     final TextView mModType; | ||||
|     final TextView mModSize; | ||||
|     final ImageView mStatusIcon; | ||||
|     final View mDownloadBtn; | ||||
|     final TextView mDownloadProgress; | ||||
|     final View mUninstall; | ||||
|  | ||||
|     ModsViewHolder(final View parentView) | ||||
|     { | ||||
|         super(LayoutInflater.from(parentView.getContext()).inflate(R.layout.mods_adapter_item, (ViewGroup) parentView, false), true); | ||||
|         mModAuthor = (TextView) itemView.findViewById(R.id.mods_adapter_item_author); | ||||
|         mModType = (TextView) itemView.findViewById(R.id.mods_adapter_item_modtype); | ||||
|         mModSize = (TextView) itemView.findViewById(R.id.mods_adapter_item_size); | ||||
|         mDownloadBtn = itemView.findViewById(R.id.mods_adapter_item_btn_download); | ||||
|         mStatusIcon = (ImageView) itemView.findViewById(R.id.mods_adapter_item_status); | ||||
|         mDownloadProgress = (TextView) itemView.findViewById(R.id.mods_adapter_item_install_progress); | ||||
|         mUninstall = itemView.findViewById(R.id.mods_adapter_item_btn_uninstall); | ||||
|     } | ||||
| } | ||||
| @@ -1,258 +0,0 @@ | ||||
| package eu.vcmi.vcmi.mods; | ||||
|  | ||||
| import android.text.TextUtils; | ||||
|  | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.Comparator; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
|  | ||||
| import eu.vcmi.vcmi.BuildConfig; | ||||
| import eu.vcmi.vcmi.util.FileUtil; | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class VCMIMod | ||||
| { | ||||
|     protected final Map<String, VCMIMod> mSubmods; | ||||
|     public String mId; | ||||
|     public String mName; | ||||
|     public String mDesc; | ||||
|     public String mVersion; | ||||
|     public String mAuthor; | ||||
|     public String mContact; | ||||
|     public String mModType; | ||||
|     public String mArchiveUrl; | ||||
|     public long mSize; | ||||
|     public File installationFolder; | ||||
|  | ||||
|     // config values | ||||
|     public boolean mActive; | ||||
|     public boolean mInstalled; | ||||
|     public boolean mValidated; | ||||
|     public String mChecksum; | ||||
|  | ||||
|     // internal | ||||
|     public boolean mLoadedCorrectly; | ||||
|     public boolean mSystem; | ||||
|  | ||||
|     protected VCMIMod() | ||||
|     { | ||||
|         mSubmods = new HashMap<>(); | ||||
|     } | ||||
|  | ||||
|     public static VCMIMod buildFromRepoJson(final String id, | ||||
|                                             final JSONObject obj, | ||||
|                                             JSONObject modDownloadData) | ||||
|     { | ||||
|         final VCMIMod mod = new VCMIMod(); | ||||
|         mod.mId = id.toLowerCase(Locale.US); | ||||
|         mod.mName = obj.optString("name"); | ||||
|         mod.mDesc = obj.optString("description"); | ||||
|         mod.mVersion = obj.optString("version"); | ||||
|         mod.mAuthor = obj.optString("author"); | ||||
|         mod.mContact = obj.optString("contact"); | ||||
|         mod.mModType = obj.optString("modType"); | ||||
|         mod.mArchiveUrl = modDownloadData.optString("download"); | ||||
|         mod.mSize = obj.optLong("size"); | ||||
|         mod.mLoadedCorrectly = true; | ||||
|         return mod; | ||||
|     } | ||||
|  | ||||
|     public static VCMIMod buildFromConfigJson(final String id, final JSONObject obj) throws JSONException | ||||
|     { | ||||
|         final VCMIMod mod = new VCMIMod(); | ||||
|         mod.updateFromConfigJson(id, obj); | ||||
|         mod.mInstalled = true; | ||||
|         return mod; | ||||
|     } | ||||
|  | ||||
|     public static VCMIMod buildFromModInfo(final File modPath) throws IOException, JSONException | ||||
|     { | ||||
|         final VCMIMod mod = new VCMIMod(); | ||||
|         if (!mod.updateFromModInfo(modPath)) | ||||
|         { | ||||
|             return mod; | ||||
|         } | ||||
|         mod.mLoadedCorrectly = true; | ||||
|         mod.mActive = true; // active by default | ||||
|         mod.mInstalled = true; | ||||
|         mod.installationFolder = modPath; | ||||
|  | ||||
|         return mod; | ||||
|     } | ||||
|  | ||||
|     protected static Map<String, VCMIMod> loadSubmods(final List<File> modsList) throws IOException, JSONException | ||||
|     { | ||||
|         final Map<String, VCMIMod> submods = new HashMap<>(); | ||||
|         for (final File f : modsList) | ||||
|         { | ||||
|             if (!f.isDirectory()) | ||||
|             { | ||||
|                 Log.w("VCMI", "Non-directory encountered in mods dir: " + f.getName()); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             final VCMIMod submod = buildFromModInfo(f); | ||||
|             if (submod == null) | ||||
|             { | ||||
|                 Log.w(null, "Could not build mod in folder " + f + "; ignoring"); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             submods.put(submod.mId, submod); | ||||
|         } | ||||
|         return submods; | ||||
|     } | ||||
|  | ||||
|     public void updateFromConfigJson(final String id, final JSONObject obj) throws JSONException | ||||
|     { | ||||
|         if(mSystem) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         mId = id.toLowerCase(Locale.US); | ||||
|         mActive = obj.optBoolean("active"); | ||||
|         mValidated = obj.optBoolean("validated"); | ||||
|         mChecksum = obj.optString("checksum"); | ||||
|  | ||||
|         final JSONObject submods = obj.optJSONObject("mods"); | ||||
|         if (submods != null) | ||||
|         { | ||||
|             updateChildrenFromConfigJson(submods); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected void updateChildrenFromConfigJson(final JSONObject submods) throws JSONException | ||||
|     { | ||||
|         final JSONArray names = submods.names(); | ||||
|         for (int i = 0; i < names.length(); ++i) | ||||
|         { | ||||
|             final String modId = names.getString(i); | ||||
|             final String normalizedModId = modId.toLowerCase(Locale.US); | ||||
|             if (!mSubmods.containsKey(normalizedModId)) | ||||
|             { | ||||
|                 Log.w(this, "Mod present in config but not found in /Mods; ignoring: " + modId); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             mSubmods.get(normalizedModId).updateFromConfigJson(modId, submods.getJSONObject(modId)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public boolean updateFromModInfo(final File modPath) throws IOException, JSONException | ||||
|     { | ||||
|         final File modInfoFile = new File(modPath, "mod.json"); | ||||
|         if (!modInfoFile.exists()) | ||||
|         { | ||||
|             Log.w(this, "Mod info doesn't exist"); | ||||
|             mName = modPath.getAbsolutePath(); | ||||
|             return false; | ||||
|         } | ||||
|         try | ||||
|         { | ||||
|             final JSONObject modInfoContent = new JSONObject(FileUtil.read(modInfoFile)); | ||||
|             mId = modPath.getName().toLowerCase(Locale.US); | ||||
|             mName = modInfoContent.optString("name"); | ||||
|             mDesc = modInfoContent.optString("description"); | ||||
|             mVersion = modInfoContent.optString("version"); | ||||
|             mAuthor = modInfoContent.optString("author"); | ||||
|             mContact = modInfoContent.optString("contact"); | ||||
|             mModType = modInfoContent.optString("modType"); | ||||
|             mSystem = mId.equals("vcmi"); | ||||
|  | ||||
|             final File submodsDir = new File(modPath, "Mods"); | ||||
|             if (submodsDir.exists()) | ||||
|             { | ||||
|                 final List<File> submodsFiles = new ArrayList<>(); | ||||
|                 Collections.addAll(submodsFiles, submodsDir.listFiles()); | ||||
|                 mSubmods.putAll(loadSubmods(submodsFiles)); | ||||
|             } | ||||
|             return true; | ||||
|         } | ||||
|         catch (final JSONException ex) | ||||
|         { | ||||
|             mName = modPath.getAbsolutePath(); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String toString() | ||||
|     { | ||||
|         if (!BuildConfig.DEBUG) | ||||
|         { | ||||
|             return ""; | ||||
|         } | ||||
|         return String.format("mod:[id:%s,active:%s,submods:[%s]]", mId, mActive, TextUtils.join(",", mSubmods.values())); | ||||
|     } | ||||
|  | ||||
|     protected void submodsToJson(final JSONObject modsRoot) throws JSONException | ||||
|     { | ||||
|         for (final VCMIMod submod : mSubmods.values()) | ||||
|         { | ||||
|             final JSONObject submodEntry = new JSONObject(); | ||||
|             submod.toJsonInternal(submodEntry); | ||||
|             modsRoot.put(submod.mId, submodEntry); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected void toJsonInternal(final JSONObject root) throws JSONException | ||||
|     { | ||||
|         root.put("active", mActive); | ||||
|         root.put("validated", mValidated); | ||||
|         if (!TextUtils.isEmpty(mChecksum)) | ||||
|         { | ||||
|             root.put("checksum", mChecksum); | ||||
|         } | ||||
|         if (!mSubmods.isEmpty()) | ||||
|         { | ||||
|             JSONObject submods = new JSONObject(); | ||||
|             submodsToJson(submods); | ||||
|             root.put("mods", submods); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public boolean hasSubmods() | ||||
|     { | ||||
|         return !mSubmods.isEmpty(); | ||||
|     } | ||||
|  | ||||
|     public List<VCMIMod> submods() | ||||
|     { | ||||
|         final ArrayList<VCMIMod> ret = new ArrayList<>(); | ||||
|  | ||||
|         ret.addAll(mSubmods.values()); | ||||
|  | ||||
|         Collections.sort(ret, new Comparator<VCMIMod>() | ||||
|         { | ||||
|             @Override | ||||
|             public int compare(VCMIMod left, VCMIMod right) | ||||
|             { | ||||
|                 return left.mName.compareTo(right.mName); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return ret; | ||||
|     } | ||||
|  | ||||
|     protected void updateFrom(VCMIMod other) | ||||
|     { | ||||
|         this.mModType = other.mModType; | ||||
|         this.mAuthor = other.mAuthor; | ||||
|         this.mDesc = other.mDesc; | ||||
|         this.mArchiveUrl = other.mArchiveUrl; | ||||
|     } | ||||
| } | ||||
| @@ -1,106 +0,0 @@ | ||||
| package eu.vcmi.vcmi.mods; | ||||
|  | ||||
| import android.text.TextUtils; | ||||
|  | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
|  | ||||
| import eu.vcmi.vcmi.BuildConfig; | ||||
| import eu.vcmi.vcmi.util.FileUtil; | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class VCMIModContainer extends VCMIMod | ||||
| { | ||||
|     private VCMIMod mCoreStatus; // kept here to correctly save core object to modSettings | ||||
|  | ||||
|     private VCMIModContainer() | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     public static VCMIModContainer createContainer(final List<File> modsList) throws IOException, JSONException | ||||
|     { | ||||
|         final VCMIModContainer mod = new VCMIModContainer(); | ||||
|         mod.mSubmods.putAll(loadSubmods(modsList)); | ||||
|         return mod; | ||||
|     } | ||||
|  | ||||
|     public void updateContainerFromConfigJson(final JSONObject modsList, final JSONObject coreStatus) throws JSONException | ||||
|     { | ||||
|         updateChildrenFromConfigJson(modsList); | ||||
|         if (coreStatus != null) | ||||
|         { | ||||
|             mCoreStatus = VCMIMod.buildFromConfigJson("core", coreStatus); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void updateFromRepo(List<VCMIMod> repoMods){ | ||||
|         for (VCMIMod mod : repoMods) | ||||
|         { | ||||
|             final String normalizedModId = mod.mId.toLowerCase(Locale.US); | ||||
|  | ||||
|             if(mSubmods.containsKey(normalizedModId)){ | ||||
|                 VCMIMod existing = mSubmods.get(normalizedModId); | ||||
|  | ||||
|                 existing.updateFrom(mod); | ||||
|             } | ||||
|             else{ | ||||
|                 mSubmods.put(normalizedModId, mod); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String toString() | ||||
|     { | ||||
|         if (!BuildConfig.DEBUG) | ||||
|         { | ||||
|             return ""; | ||||
|         } | ||||
|         return String.format("mods:[%s]", TextUtils.join(",", mSubmods.values())); | ||||
|     } | ||||
|  | ||||
|     public void saveToFile(final File location) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             FileUtil.write(location, toJson()); | ||||
|         } | ||||
|         catch (Exception e) | ||||
|         { | ||||
|             Log.e(this, "Could not save mod settings", e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected String toJson() throws JSONException | ||||
|     { | ||||
|         final JSONObject root = new JSONObject(); | ||||
|         final JSONObject activeMods = new JSONObject(); | ||||
|         final JSONObject coreStatus = new JSONObject(); | ||||
|         root.put("activeMods", activeMods); | ||||
|         submodsToJson(activeMods); | ||||
|  | ||||
|         coreStatusToJson(coreStatus); | ||||
|         root.put("core", coreStatus); | ||||
|  | ||||
|         return root.toString(); | ||||
|     } | ||||
|  | ||||
|     private void coreStatusToJson(final JSONObject coreStatus) throws JSONException | ||||
|     { | ||||
|         if (mCoreStatus == null) | ||||
|         { | ||||
|             mCoreStatus = new VCMIMod(); | ||||
|             mCoreStatus.mId = "core"; | ||||
|             mCoreStatus.mActive = true; | ||||
|         } | ||||
|         mCoreStatus.toJsonInternal(coreStatus); | ||||
|     } | ||||
| } | ||||
| @@ -1,108 +0,0 @@ | ||||
| package eu.vcmi.vcmi.mods; | ||||
|  | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import eu.vcmi.vcmi.util.AsyncRequest; | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
| import eu.vcmi.vcmi.util.ServerResponse; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class VCMIModsRepo | ||||
| { | ||||
|     private final List<VCMIMod> mModsList; | ||||
|     private IOnModsRepoDownloaded mCallback; | ||||
|  | ||||
|     public VCMIModsRepo() | ||||
|     { | ||||
|         mModsList = new ArrayList<>(); | ||||
|     } | ||||
|  | ||||
|     public void init(final String url, final IOnModsRepoDownloaded callback) | ||||
|     { | ||||
|         mCallback = callback; | ||||
|         new AsyncLoadRepo().execute(url); | ||||
|     } | ||||
|  | ||||
|     public interface IOnModsRepoDownloaded | ||||
|     { | ||||
|         void onSuccess(ServerResponse<List<VCMIMod>> response); | ||||
|         void onError(final int code); | ||||
|     } | ||||
|  | ||||
|     private class AsyncLoadRepo extends AsyncRequest<List<VCMIMod>> | ||||
|     { | ||||
|         @Override | ||||
|         protected ServerResponse<List<VCMIMod>> doInBackground(final String... params) | ||||
|         { | ||||
|             ServerResponse<List<VCMIMod>> serverResponse = sendRequest(params[0]); | ||||
|             if (serverResponse.isValid()) | ||||
|             { | ||||
|                 final List<VCMIMod> mods = new ArrayList<>(); | ||||
|                 try | ||||
|                 { | ||||
|                     JSONObject jsonContent = new JSONObject(serverResponse.mRawContent); | ||||
|                     final JSONArray names = jsonContent.names(); | ||||
|                     for (int i = 0; i < names.length(); ++i) | ||||
|                     { | ||||
|                         try | ||||
|                         { | ||||
|                             String name = names.getString(i); | ||||
|                             JSONObject modDownloadData = jsonContent.getJSONObject(name); | ||||
|  | ||||
|                             if(modDownloadData.has("mod")) | ||||
|                             { | ||||
|                                 String modFileAddress = modDownloadData.getString("mod"); | ||||
|                                 ServerResponse<List<VCMIMod>> modFile = sendRequest(modFileAddress); | ||||
|  | ||||
|                                 if (!modFile.isValid()) | ||||
|                                 { | ||||
|                                     continue; | ||||
|                                 } | ||||
|  | ||||
|                                 JSONObject modJson = new JSONObject(modFile.mRawContent); | ||||
|                                 mods.add(VCMIMod.buildFromRepoJson(name, modJson, modDownloadData)); | ||||
|                             } | ||||
|                             else | ||||
|                             { | ||||
|                                 mods.add(VCMIMod.buildFromRepoJson(name, modDownloadData, modDownloadData)); | ||||
|                             } | ||||
|                         } | ||||
|                         catch (JSONException e) | ||||
|                         { | ||||
|                             Log.e(this, "Could not parse the response as json", e); | ||||
|                         } | ||||
|                     } | ||||
|                     serverResponse.mContent = mods; | ||||
|                 } | ||||
|                 catch (JSONException e) | ||||
|                 { | ||||
|                     Log.e(this, "Could not parse the response as json", e); | ||||
|                     serverResponse.mCode = ServerResponse.LOCAL_ERROR_PARSING; | ||||
|                 } | ||||
|             } | ||||
|             return serverResponse; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void onPostExecute(final ServerResponse<List<VCMIMod>> response) | ||||
|         { | ||||
|             if (response.isValid()) | ||||
|             { | ||||
|                 mModsList.clear(); | ||||
|                 mModsList.addAll(response.mContent); | ||||
|                 mCallback.onSuccess(response); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 mCallback.onError(response.mCode); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,46 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import eu.vcmi.vcmi.Config; | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class AdventureAiController extends LauncherSettingWithDialogController<String, Config> | ||||
| { | ||||
|     public AdventureAiController(final AppCompatActivity activity) | ||||
|     { | ||||
|         super(activity); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected LauncherSettingDialog<String> dialog() | ||||
|     { | ||||
|         return new AdventureAiSelectionDialog(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onItemChosen(final String item) | ||||
|     { | ||||
|         mConfig.setAdventureAi(item); | ||||
|         updateContent(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String mainText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_adventure_ai_title); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String subText() | ||||
|     { | ||||
|         if (mConfig == null) | ||||
|         { | ||||
|             return ""; | ||||
|         } | ||||
|  | ||||
|         return mConfig.getAdventureAi(); | ||||
|     } | ||||
| } | ||||
| @@ -1,37 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class AdventureAiSelectionDialog extends LauncherSettingDialog<String> | ||||
| { | ||||
|     private static final List<String> AVAILABLE_AI = new ArrayList<>(); | ||||
|  | ||||
|     static | ||||
|     { | ||||
|         AVAILABLE_AI.add("VCAI"); | ||||
|         AVAILABLE_AI.add("Nullkiller"); | ||||
|     } | ||||
|  | ||||
|     public AdventureAiSelectionDialog() | ||||
|     { | ||||
|         super(AVAILABLE_AI); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected int dialogTitleResId() | ||||
|     { | ||||
|         return R.string.launcher_btn_adventure_ai_title; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected CharSequence itemName(final String item) | ||||
|     { | ||||
|         return item; | ||||
|     } | ||||
| } | ||||
| @@ -1,193 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.Environment; | ||||
| import android.provider.DocumentsContract; | ||||
| import android.view.View; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.io.OutputStream; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.documentfile.provider.DocumentFile; | ||||
| import androidx.loader.content.AsyncTaskLoader; | ||||
| import eu.vcmi.vcmi.R; | ||||
| import eu.vcmi.vcmi.Storage; | ||||
| import eu.vcmi.vcmi.util.FileUtil; | ||||
|  | ||||
| public class CopyDataController extends LauncherSettingController<Void, Void> | ||||
| { | ||||
|     public static final int PICK_EXTERNAL_VCMI_DATA_TO_COPY = 3; | ||||
|  | ||||
|     private String progress; | ||||
|  | ||||
|     public CopyDataController(final AppCompatActivity act) | ||||
|     { | ||||
|         super(act); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String mainText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_import_title); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String subText() | ||||
|     { | ||||
|         if (progress != null) | ||||
|         { | ||||
|             return progress; | ||||
|         } | ||||
|  | ||||
|         return mActivity.getString(R.string.launcher_btn_import_description); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onClick(final View v) | ||||
|     { | ||||
|         Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); | ||||
|  | ||||
|         intent.putExtra( | ||||
|                 DocumentsContract.EXTRA_INITIAL_URI, | ||||
|                 Uri.fromFile(new File(Environment.getExternalStorageDirectory(), "vcmi-data"))); | ||||
|  | ||||
|         mActivity.startActivityForResult(intent, PICK_EXTERNAL_VCMI_DATA_TO_COPY); | ||||
|     } | ||||
|  | ||||
|     public void copyData(Uri folderToCopy) | ||||
|     { | ||||
|         AsyncCopyData copyTask = new AsyncCopyData(mActivity, folderToCopy); | ||||
|  | ||||
|         copyTask.execute(); | ||||
|     } | ||||
|  | ||||
|     private class AsyncCopyData extends AsyncTask<String, String, Boolean> | ||||
|     { | ||||
|         private Activity owner; | ||||
|         private Uri folderToCopy; | ||||
|  | ||||
|         public AsyncCopyData(Activity owner, Uri folderToCopy) | ||||
|         { | ||||
|             this.owner = owner; | ||||
|             this.folderToCopy = folderToCopy; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected Boolean doInBackground(final String... params) | ||||
|         { | ||||
|             File targetDir = Storage.getVcmiDataDir(owner); | ||||
|             DocumentFile sourceDir = DocumentFile.fromTreeUri(owner, folderToCopy); | ||||
|  | ||||
|             ArrayList<String> allowedFolders = new ArrayList<String>(); | ||||
|  | ||||
|             allowedFolders.add("Data"); | ||||
|             allowedFolders.add("Mp3"); | ||||
|             allowedFolders.add("Maps"); | ||||
|             allowedFolders.add("Saves"); | ||||
|             allowedFolders.add("Mods"); | ||||
|             allowedFolders.add("config"); | ||||
|  | ||||
|             return copyDirectory(targetDir, sourceDir, allowedFolders); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void onPostExecute(Boolean result) | ||||
|         { | ||||
|             super.onPostExecute(result); | ||||
|  | ||||
|             if (result) | ||||
|             { | ||||
|                 CopyDataController.this.progress = null; | ||||
|                 CopyDataController.this.updateContent(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void onProgressUpdate(String... values) | ||||
|         { | ||||
|             CopyDataController.this.progress = values[0]; | ||||
|             CopyDataController.this.updateContent(); | ||||
|         } | ||||
|  | ||||
|         private boolean copyDirectory(File targetDir, DocumentFile sourceDir, List<String> allowed) | ||||
|         { | ||||
|             if (!targetDir.exists()) | ||||
|             { | ||||
|                 targetDir.mkdir(); | ||||
|             } | ||||
|  | ||||
|             for (DocumentFile child : sourceDir.listFiles()) | ||||
|             { | ||||
|                 if (allowed != null) | ||||
|                 { | ||||
|                     boolean fileAllowed = false; | ||||
|                      | ||||
|                     for (String str : allowed) | ||||
|                     { | ||||
|                         if (str.equalsIgnoreCase(child.getName())) | ||||
|                         { | ||||
|                             fileAllowed = true; | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     if (!fileAllowed) | ||||
|                         continue; | ||||
|                 } | ||||
|  | ||||
|                 File exported = new File(targetDir, child.getName()); | ||||
|  | ||||
|                 if (child.isFile()) | ||||
|                 { | ||||
|                     publishProgress(owner.getString(R.string.launcher_progress_copy, | ||||
|                             child.getName())); | ||||
|  | ||||
|                     if (!exported.exists()) | ||||
|                     { | ||||
|                         try | ||||
|                         { | ||||
|                             exported.createNewFile(); | ||||
|                         } | ||||
|                         catch (IOException e) | ||||
|                         { | ||||
|                             publishProgress("Failed to copy file " + child.getName()); | ||||
|  | ||||
|                             return false; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     try ( | ||||
|                         final OutputStream targetStream = new FileOutputStream(exported, false); | ||||
|                         final InputStream sourceStream = owner.getContentResolver() | ||||
|                                 .openInputStream(child.getUri())) | ||||
|                     { | ||||
|                         FileUtil.copyStream(sourceStream, targetStream); | ||||
|                     } | ||||
|                     catch (IOException e) | ||||
|                     { | ||||
|                         publishProgress("Failed to copy file " + child.getName()); | ||||
|  | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (child.isDirectory() && !copyDirectory(exported, child, null)) | ||||
|                 { | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -1,19 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import eu.vcmi.vcmi.Config; | ||||
| import eu.vcmi.vcmi.util.SharedPrefs; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class DoubleConfig | ||||
| { | ||||
|     public final Config mConfig; | ||||
|     public final SharedPrefs mPrefs; | ||||
|  | ||||
|     public DoubleConfig(final Config config, final SharedPrefs prefs) | ||||
|     { | ||||
|         mConfig = config; | ||||
|         mPrefs = prefs; | ||||
|     } | ||||
| } | ||||
| @@ -1,174 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.Environment; | ||||
| import android.provider.DocumentsContract; | ||||
| import android.view.View; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.FileInputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.io.OutputStream; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.documentfile.provider.DocumentFile; | ||||
| import eu.vcmi.vcmi.R; | ||||
| import eu.vcmi.vcmi.Storage; | ||||
| import eu.vcmi.vcmi.util.FileUtil; | ||||
|  | ||||
| public class ExportDataController extends LauncherSettingController<Void, Void> | ||||
| { | ||||
|     public static final int PICK_DIRECTORY_TO_EXPORT = 4; | ||||
|  | ||||
|     private String progress; | ||||
|  | ||||
|     public ExportDataController(final AppCompatActivity act) | ||||
|     { | ||||
|         super(act); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String mainText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_export_title); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String subText() | ||||
|     { | ||||
|         if (progress != null) | ||||
|         { | ||||
|             return progress; | ||||
|         } | ||||
|  | ||||
|         return mActivity.getString(R.string.launcher_btn_export_description); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onClick(final View v) | ||||
|     { | ||||
|         Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); | ||||
|  | ||||
|         intent.putExtra( | ||||
|             DocumentsContract.EXTRA_INITIAL_URI, | ||||
|             Uri.fromFile(new File(Environment.getExternalStorageDirectory(), "vcmi-data"))); | ||||
|  | ||||
|         mActivity.startActivityForResult(intent, PICK_DIRECTORY_TO_EXPORT); | ||||
|     } | ||||
|  | ||||
|     public void copyData(Uri targetFolder) | ||||
|     { | ||||
|         AsyncCopyData copyTask = new AsyncCopyData(mActivity, targetFolder); | ||||
|  | ||||
|         copyTask.execute(); | ||||
|     } | ||||
|  | ||||
|     private class AsyncCopyData extends AsyncTask<String, String, Boolean> | ||||
|     { | ||||
|         private Activity owner; | ||||
|         private Uri targetFolder; | ||||
|  | ||||
|         public AsyncCopyData(Activity owner, Uri targetFolder) | ||||
|         { | ||||
|             this.owner = owner; | ||||
|             this.targetFolder = targetFolder; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected Boolean doInBackground(final String... params) | ||||
|         { | ||||
|             File targetDir = Storage.getVcmiDataDir(owner); | ||||
|             DocumentFile sourceDir = DocumentFile.fromTreeUri(owner, targetFolder); | ||||
|  | ||||
|             return copyDirectory(targetDir, sourceDir); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void onPostExecute(Boolean result) | ||||
|         { | ||||
|             super.onPostExecute(result); | ||||
|  | ||||
|             if (result) | ||||
|             { | ||||
|                 ExportDataController.this.progress = null; | ||||
|                 ExportDataController.this.updateContent(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         protected void onProgressUpdate(String... values) | ||||
|         { | ||||
|             ExportDataController.this.progress = values[0]; | ||||
|             ExportDataController.this.updateContent(); | ||||
|         } | ||||
|  | ||||
|         private boolean copyDirectory(File sourceDir, DocumentFile targetDir) | ||||
|         { | ||||
|             for (File child : sourceDir.listFiles()) | ||||
|             { | ||||
|                 DocumentFile exported = targetDir.findFile(child.getName()); | ||||
|  | ||||
|                 if (child.isFile()) | ||||
|                 { | ||||
|                     publishProgress(owner.getString(R.string.launcher_progress_copy, | ||||
|                             child.getName())); | ||||
|  | ||||
|                     if (exported == null) | ||||
|                     { | ||||
|                         try | ||||
|                         { | ||||
|                             exported = targetDir.createFile( | ||||
|                                 "application/octet-stream", | ||||
|                                 child.getName()); | ||||
|                         } | ||||
|                         catch (UnsupportedOperationException e) | ||||
|                         { | ||||
|                             publishProgress("Failed to copy file " + child.getName()); | ||||
|  | ||||
|                             return false; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (exported == null) | ||||
|                     { | ||||
|                         publishProgress("Failed to copy file " + child.getName()); | ||||
|                         return false; | ||||
|                     } | ||||
|  | ||||
|                     try( | ||||
|                             final OutputStream targetStream = owner.getContentResolver() | ||||
|                                     .openOutputStream(exported.getUri()); | ||||
|                             final InputStream sourceStream = new FileInputStream(child)) | ||||
|                     { | ||||
|                         FileUtil.copyStream(sourceStream, targetStream); | ||||
|                     } | ||||
|                     catch (IOException e) | ||||
|                     { | ||||
|                         publishProgress("Failed to copy file " + child.getName()); | ||||
|  | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (child.isDirectory()) | ||||
|                 { | ||||
|                     if (exported == null) | ||||
|                     { | ||||
|                         exported = targetDir.createDirectory(child.getName()); | ||||
|                     } | ||||
|  | ||||
|                     if(!copyDirectory(child, exported)) | ||||
|                     { | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,48 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
|  | ||||
| import eu.vcmi.vcmi.Config; | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class LanguageSettingController extends LauncherSettingWithDialogController<String, Config> | ||||
| { | ||||
|     public LanguageSettingController(final AppCompatActivity activity) | ||||
|     { | ||||
|         super(activity); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected LauncherSettingDialog<String> dialog() | ||||
|     { | ||||
|         return new LanguageSettingDialog(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onItemChosen(final String item) | ||||
|     { | ||||
|         mConfig.updateLanguage(item); | ||||
|         updateContent(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String mainText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_language_title); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String subText() | ||||
|     { | ||||
|         if (mConfig == null) | ||||
|         { | ||||
|             return ""; | ||||
|         } | ||||
|         return mConfig.mLanguage == null || mConfig.mLanguage.isEmpty() | ||||
|                ? mActivity.getString(R.string.launcher_btn_language_subtitle_unknown) | ||||
|                : mActivity.getString(R.string.launcher_btn_language_subtitle, mConfig.mLanguage); | ||||
|     } | ||||
| } | ||||
| @@ -1,55 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class LanguageSettingDialog extends LauncherSettingDialog<String> | ||||
| { | ||||
|     private static final List<String> AVAILABLE_LANGUAGES = new ArrayList<>(); | ||||
|  | ||||
|     static | ||||
|     { | ||||
|         AVAILABLE_LANGUAGES.add("english"); | ||||
|         AVAILABLE_LANGUAGES.add("czech"); | ||||
|         AVAILABLE_LANGUAGES.add("chinese"); | ||||
|         AVAILABLE_LANGUAGES.add("finnish"); | ||||
|         AVAILABLE_LANGUAGES.add("french"); | ||||
|         AVAILABLE_LANGUAGES.add("german"); | ||||
|         AVAILABLE_LANGUAGES.add("hungarian"); | ||||
|         AVAILABLE_LANGUAGES.add("italian"); | ||||
|         AVAILABLE_LANGUAGES.add("korean"); | ||||
|         AVAILABLE_LANGUAGES.add("polish"); | ||||
|         AVAILABLE_LANGUAGES.add("portuguese"); | ||||
|         AVAILABLE_LANGUAGES.add("russian"); | ||||
|         AVAILABLE_LANGUAGES.add("spanish"); | ||||
|         AVAILABLE_LANGUAGES.add("swedish"); | ||||
|         AVAILABLE_LANGUAGES.add("turkish"); | ||||
|         AVAILABLE_LANGUAGES.add("ukrainian"); | ||||
|         AVAILABLE_LANGUAGES.add("vietnamese"); | ||||
|         AVAILABLE_LANGUAGES.add("other_cp1250"); | ||||
|         AVAILABLE_LANGUAGES.add("other_cp1251"); | ||||
|         AVAILABLE_LANGUAGES.add("other_cp1252"); | ||||
|     } | ||||
|  | ||||
|     public LanguageSettingDialog() | ||||
|     { | ||||
|         super(AVAILABLE_LANGUAGES); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected int dialogTitleResId() | ||||
|     { | ||||
|         return R.string.launcher_btn_language_title; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected CharSequence itemName(final String item) | ||||
|     { | ||||
|         return item; | ||||
|     } | ||||
| } | ||||
| @@ -1,75 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import android.view.View; | ||||
| import android.widget.TextView; | ||||
|  | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public abstract class LauncherSettingController<TSetting, TConf> implements View.OnClickListener | ||||
| { | ||||
|     protected AppCompatActivity mActivity; | ||||
|     protected TConf mConfig; | ||||
|     private View mSettingViewRoot; | ||||
|     private TextView mSettingsTextMain; | ||||
|     private TextView mSettingsTextSub; | ||||
|  | ||||
|     LauncherSettingController(final AppCompatActivity act) | ||||
|     { | ||||
|         mActivity = act; | ||||
|     } | ||||
|  | ||||
|     public final LauncherSettingController<TSetting, TConf> init(final int rootViewResId) | ||||
|     { | ||||
|         return init(rootViewResId, null); | ||||
|     } | ||||
|  | ||||
|     public final LauncherSettingController<TSetting, TConf> init(final int rootViewResId, final TConf config) | ||||
|     { | ||||
|         mSettingViewRoot = mActivity.findViewById(rootViewResId); | ||||
|         mSettingViewRoot.setOnClickListener(this); | ||||
|         mSettingsTextMain = (TextView) mSettingViewRoot.findViewById(R.id.inc_launcher_btn_main); | ||||
|         mSettingsTextSub = (TextView) mSettingViewRoot.findViewById(R.id.inc_launcher_btn_sub); | ||||
|         childrenInit(mSettingViewRoot); | ||||
|         updateConfig(config); | ||||
|         updateContent(); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     protected void childrenInit(final View root) | ||||
|     { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public void updateConfig(final TConf conf) | ||||
|     { | ||||
|         mConfig = conf; | ||||
|         updateContent(); | ||||
|     } | ||||
|  | ||||
|     public void updateContent() | ||||
|     { | ||||
|         mSettingsTextMain.setText(mainText()); | ||||
|         if (mSettingsTextSub != null) | ||||
|         { | ||||
|             mSettingsTextSub.setText(subText()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected abstract String mainText(); | ||||
|  | ||||
|     protected abstract String subText(); | ||||
|  | ||||
|     public void show() | ||||
|     { | ||||
|         mSettingViewRoot.setVisibility(View.VISIBLE); | ||||
|     } | ||||
|  | ||||
|     public void hide() | ||||
|     { | ||||
|         mSettingViewRoot.setVisibility(View.GONE); | ||||
|     } | ||||
| } | ||||
| @@ -1,73 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import android.app.Dialog; | ||||
| import android.content.DialogInterface; | ||||
| import android.os.Bundle; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.fragment.app.DialogFragment; | ||||
| import androidx.appcompat.app.AlertDialog; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public abstract class LauncherSettingDialog<T> extends DialogFragment | ||||
| { | ||||
|     protected final List<T> mDataset; | ||||
|     private IOnItemChosen<T> mObserver; | ||||
|  | ||||
|     protected LauncherSettingDialog(final List<T> dataset) | ||||
|     { | ||||
|         mDataset = dataset; | ||||
|     } | ||||
|  | ||||
|     public void observe(final IOnItemChosen<T> observer) | ||||
|     { | ||||
|         mObserver = observer; | ||||
|     } | ||||
|  | ||||
|     protected abstract CharSequence itemName(T item); | ||||
|  | ||||
|     protected abstract int dialogTitleResId(); | ||||
|  | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Dialog onCreateDialog(final Bundle savedInstanceState) | ||||
|     { | ||||
|         List<CharSequence> list = new ArrayList<>(); | ||||
|  | ||||
|         for (T t : mDataset) | ||||
|         { | ||||
|             CharSequence charSequence = itemName(t); | ||||
|             list.add(charSequence); | ||||
|         } | ||||
|  | ||||
|         return new AlertDialog.Builder(getActivity()) | ||||
|             .setTitle(dialogTitleResId()) | ||||
|             .setItems( | ||||
|                     list.toArray(new CharSequence[0]), | ||||
|                     this::onItemChosenInternal) | ||||
|             .create(); | ||||
|     } | ||||
|  | ||||
|     private void onItemChosenInternal(final DialogInterface dialog, final int index) | ||||
|     { | ||||
|         final T chosenItem = mDataset.get(index); | ||||
|         Log.d(this, "Chosen item: " + chosenItem); | ||||
|         dialog.dismiss(); | ||||
|         if (mObserver != null) | ||||
|         { | ||||
|             mObserver.onItemChosen(chosenItem); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public interface IOnItemChosen<V> | ||||
|     { | ||||
|         void onItemChosen(V item); | ||||
|     } | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import android.view.View; | ||||
|  | ||||
| import eu.vcmi.vcmi.util.Log; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public abstract class LauncherSettingWithDialogController<T, Conf> extends LauncherSettingController<T, Conf> | ||||
|     implements LauncherSettingDialog.IOnItemChosen<T> | ||||
| { | ||||
|     public static final String SETTING_DIALOG_ID = "settings.dialog"; | ||||
|  | ||||
|     protected LauncherSettingWithDialogController(final AppCompatActivity act) | ||||
|     { | ||||
|         super(act); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onClick(final View v) | ||||
|     { | ||||
|         Log.i(this, "Showing dialog"); | ||||
|         final LauncherSettingDialog<T> dialog = dialog(); | ||||
|         dialog.observe(this); // TODO rebinding dialogs on activity config changes | ||||
|         dialog.show(mActivity.getSupportFragmentManager(), SETTING_DIALOG_ID); | ||||
|     } | ||||
|  | ||||
|     protected abstract LauncherSettingDialog<T> dialog(); | ||||
| } | ||||
| @@ -1,83 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import androidx.appcompat.widget.AppCompatSeekBar; | ||||
| import android.view.View; | ||||
| import android.widget.SeekBar; | ||||
|  | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public abstract class LauncherSettingWithSliderController<T, Conf> extends LauncherSettingController<T, Conf> | ||||
| { | ||||
|     private AppCompatSeekBar mSlider; | ||||
|     private final int mSliderMin; | ||||
|     private final int mSliderMax; | ||||
|  | ||||
|     protected LauncherSettingWithSliderController(final AppCompatActivity act, final int min, final int max) | ||||
|     { | ||||
|         super(act); | ||||
|         mSliderMin = min; | ||||
|         mSliderMax = max; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void childrenInit(final View root) | ||||
|     { | ||||
|         mSlider = (AppCompatSeekBar) root.findViewById(R.id.inc_launcher_btn_slider); | ||||
|         if (mSliderMax <= mSliderMin) | ||||
|         { | ||||
|             throw new IllegalArgumentException("slider min>=max"); | ||||
|         } | ||||
|         mSlider.setMax(mSliderMax - mSliderMin); | ||||
|         mSlider.setOnSeekBarChangeListener(new OnValueChangedListener()); | ||||
|     } | ||||
|  | ||||
|     protected abstract void onValueChanged(final int v); | ||||
|     protected abstract int currentValue(); | ||||
|  | ||||
|     @Override | ||||
|     public void updateContent() | ||||
|     { | ||||
|         super.updateContent(); | ||||
|         mSlider.setProgress(currentValue() + mSliderMin); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String subText() | ||||
|     { | ||||
|         return null; // not used with slider settings | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onClick(final View v) | ||||
|     { | ||||
|         // not used with slider settings | ||||
|     } | ||||
|  | ||||
|     private class OnValueChangedListener implements SeekBar.OnSeekBarChangeListener | ||||
|     { | ||||
|         @Override | ||||
|         public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser) | ||||
|         { | ||||
|             if (fromUser) | ||||
|             { | ||||
|                 onValueChanged(progress); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onStartTrackingTouch(final SeekBar seekBar) | ||||
|         { | ||||
|  | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onStopTrackingTouch(final SeekBar seekBar) | ||||
|         { | ||||
|  | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import android.view.View; | ||||
|  | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ModsBtnController extends LauncherSettingController<Void, Void> | ||||
| { | ||||
|     private View.OnClickListener mOnSelectedAction; | ||||
|  | ||||
|     public ModsBtnController(final AppCompatActivity act, final View.OnClickListener onSelectedAction) | ||||
|     { | ||||
|         super(act); | ||||
|         mOnSelectedAction = onSelectedAction; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String mainText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_mods_title); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String subText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_mods_subtitle); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onClick(final View v) | ||||
|     { | ||||
|         mOnSelectedAction.onClick(v); | ||||
|     } | ||||
| } | ||||
| @@ -1,40 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
|  | ||||
| import eu.vcmi.vcmi.Config; | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class MusicSettingController extends LauncherSettingWithSliderController<Integer, Config> | ||||
| { | ||||
|     public MusicSettingController(final AppCompatActivity act) | ||||
|     { | ||||
|         super(act, 0, 100); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onValueChanged(final int v) | ||||
|     { | ||||
|         mConfig.updateMusic(v); | ||||
|         updateContent(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected int currentValue() | ||||
|     { | ||||
|         if (mConfig == null) | ||||
|         { | ||||
|             return Config.DEFAULT_MUSIC_VALUE; | ||||
|         } | ||||
|         return mConfig.mVolumeMusic; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String mainText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_music_title); | ||||
|     } | ||||
| } | ||||
| @@ -1,63 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import eu.vcmi.vcmi.Config; | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class PointerModeSettingController | ||||
|     extends LauncherSettingWithDialogController<PointerModeSettingController.PointerMode, Config> | ||||
| { | ||||
|     public PointerModeSettingController(final AppCompatActivity activity) | ||||
|     { | ||||
|         super(activity); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected LauncherSettingDialog<PointerMode> dialog() | ||||
|     { | ||||
|         return new PointerModeSettingDialog(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onItemChosen(final PointerMode item) | ||||
|     { | ||||
|         mConfig.setPointerMode(item == PointerMode.RELATIVE); | ||||
|         updateContent(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String mainText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_pointermode_title); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String subText() | ||||
|     { | ||||
|         if (mConfig == null) | ||||
|         { | ||||
|             return ""; | ||||
|         } | ||||
|         return mActivity.getString(R.string.launcher_btn_pointermode_subtitle, | ||||
|             PointerModeSettingDialog.pointerModeToUserString(mActivity, getPointerMode())); | ||||
|     } | ||||
|  | ||||
|     private PointerMode getPointerMode() | ||||
|     { | ||||
|         if(mConfig.getPointerModeIsRelative()) | ||||
|         { | ||||
|             return PointerMode.RELATIVE; | ||||
|         } | ||||
|  | ||||
|         return PointerMode.NORMAL; | ||||
|     } | ||||
|  | ||||
|     public enum PointerMode | ||||
|     { | ||||
|         NORMAL, | ||||
|         RELATIVE; | ||||
|     } | ||||
| } | ||||
| @@ -1,55 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import android.content.Context; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class PointerModeSettingDialog extends LauncherSettingDialog<PointerModeSettingController.PointerMode> | ||||
| { | ||||
|     private static final List<PointerModeSettingController.PointerMode> POINTER_MODES = new ArrayList<>(); | ||||
|  | ||||
|     static | ||||
|     { | ||||
|         POINTER_MODES.add(PointerModeSettingController.PointerMode.NORMAL); | ||||
|         POINTER_MODES.add(PointerModeSettingController.PointerMode.RELATIVE); | ||||
|     } | ||||
|  | ||||
|     public PointerModeSettingDialog() | ||||
|     { | ||||
|         super(POINTER_MODES); | ||||
|     } | ||||
|  | ||||
|     public static String pointerModeToUserString( | ||||
|             final Context ctx, | ||||
|             final PointerModeSettingController.PointerMode pointerMode) | ||||
|     { | ||||
|         switch (pointerMode) | ||||
|         { | ||||
|             default: | ||||
|                 return ""; | ||||
|             case NORMAL: | ||||
|                 return ctx.getString(R.string.misc_pointermode_normal); | ||||
|             case RELATIVE: | ||||
|                 return ctx.getString(R.string.misc_pointermode_relative); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected int dialogTitleResId() | ||||
|     { | ||||
|         return R.string.launcher_btn_pointermode_title; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected CharSequence itemName(final PointerModeSettingController.PointerMode item) | ||||
|     { | ||||
|         return pointerModeToUserString(getContext(), item); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,51 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
|  | ||||
| import eu.vcmi.vcmi.Config; | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class PointerMultiplierSettingController | ||||
|         extends LauncherSettingWithDialogController<Float, Config> | ||||
| { | ||||
|     public PointerMultiplierSettingController(final AppCompatActivity activity) | ||||
|     { | ||||
|         super(activity); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected LauncherSettingDialog<Float> dialog() | ||||
|     { | ||||
|         return new PointerMultiplierSettingDialog(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onItemChosen(final Float item) | ||||
|     { | ||||
|         mConfig.setPointerSpeedMultiplier(item); | ||||
|         updateContent(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String mainText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_pointermulti_title); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String subText() | ||||
|     { | ||||
|         if (mConfig == null) | ||||
|         { | ||||
|             return ""; | ||||
|         } | ||||
|  | ||||
|         String pointerModeString = PointerMultiplierSettingDialog.pointerMultiplierToUserString( | ||||
|                 mConfig.getPointerSpeedMultiplier()); | ||||
|  | ||||
|         return mActivity.getString(R.string.launcher_btn_pointermulti_subtitle, pointerModeString); | ||||
|     } | ||||
| } | ||||
| @@ -1,48 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
|  | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class PointerMultiplierSettingDialog extends LauncherSettingDialog<Float> | ||||
| { | ||||
|     private static final List<Float> AVAILABLE_MULTIPLIERS = new ArrayList<>(); | ||||
|  | ||||
|     static | ||||
|     { | ||||
|         AVAILABLE_MULTIPLIERS.add(1.0f); | ||||
|         AVAILABLE_MULTIPLIERS.add(1.25f); | ||||
|         AVAILABLE_MULTIPLIERS.add(1.5f); | ||||
|         AVAILABLE_MULTIPLIERS.add(1.75f); | ||||
|         AVAILABLE_MULTIPLIERS.add(2.0f); | ||||
|         AVAILABLE_MULTIPLIERS.add(2.5f); | ||||
|         AVAILABLE_MULTIPLIERS.add(3.0f); | ||||
|     } | ||||
|  | ||||
|     public PointerMultiplierSettingDialog() | ||||
|     { | ||||
|         super(AVAILABLE_MULTIPLIERS); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected int dialogTitleResId() | ||||
|     { | ||||
|         return R.string.launcher_btn_pointermode_title; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected CharSequence itemName(final Float item) | ||||
|     { | ||||
|         return pointerMultiplierToUserString(item); | ||||
|     } | ||||
|  | ||||
|     public static String pointerMultiplierToUserString(final float multiplier) | ||||
|     { | ||||
|         return String.format(Locale.US, "%.2fx", multiplier); | ||||
|     } | ||||
| } | ||||
| @@ -1,64 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
|  | ||||
| import eu.vcmi.vcmi.Config; | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ScreenScaleSettingController extends LauncherSettingWithDialogController<ScreenScaleSettingController.ScreenScale, Config> | ||||
| { | ||||
|     public ScreenScaleSettingController(final AppCompatActivity activity) | ||||
|     { | ||||
|         super(activity); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected LauncherSettingDialog<ScreenScale> dialog() | ||||
|     { | ||||
|         return new ScreenScaleSettingDialog(mActivity); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onItemChosen(final ScreenScale item) | ||||
|     { | ||||
|         mConfig.updateScreenScale(item.mScreenScale); | ||||
|         updateContent(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String mainText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_scale_title); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String subText() | ||||
|     { | ||||
|         if (mConfig == null) | ||||
|         { | ||||
|             return ""; | ||||
|         } | ||||
|         return mConfig.mScreenScale <= 0 | ||||
|                ? mActivity.getString(R.string.launcher_btn_scale_subtitle_unknown) | ||||
|                : mActivity.getString(R.string.launcher_btn_scale_subtitle, mConfig.mScreenScale); | ||||
|     } | ||||
|  | ||||
|     public static class ScreenScale | ||||
|     { | ||||
|         public int mScreenScale; | ||||
|  | ||||
|         public ScreenScale(final int scale) | ||||
|         { | ||||
|             mScreenScale = scale; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public String toString() | ||||
|         { | ||||
|             return mScreenScale + "%"; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,98 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import android.app.Activity; | ||||
| import android.graphics.Point; | ||||
| import android.view.WindowMetrics; | ||||
|  | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONObject; | ||||
| import java.io.File; | ||||
| import java.util.ArrayDeque; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Queue; | ||||
| import eu.vcmi.vcmi.R; | ||||
| import eu.vcmi.vcmi.Storage; | ||||
| import eu.vcmi.vcmi.util.FileUtil; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ScreenScaleSettingDialog extends LauncherSettingDialog<ScreenScaleSettingController.ScreenScale> | ||||
| { | ||||
|     public ScreenScaleSettingDialog(Activity mActivity) | ||||
|     { | ||||
|         super(loadScales(mActivity)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected int dialogTitleResId() | ||||
|     { | ||||
|         return R.string.launcher_btn_scale_title; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected CharSequence itemName(final ScreenScaleSettingController.ScreenScale item) | ||||
|     { | ||||
|         return item.toString(); | ||||
|     } | ||||
|  | ||||
|     public static int[] getSupportedScalingRange(Activity activity) { | ||||
|         Point screenRealSize = new Point(); | ||||
|         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { | ||||
|             WindowMetrics windowMetrics = activity.getWindowManager().getCurrentWindowMetrics(); | ||||
|             screenRealSize.x = windowMetrics.getBounds().width(); | ||||
|             screenRealSize.y = windowMetrics.getBounds().height(); | ||||
|         } else { | ||||
|             activity.getWindowManager().getDefaultDisplay().getRealSize(screenRealSize); | ||||
|         } | ||||
|  | ||||
|         if (screenRealSize.x < screenRealSize.y) { | ||||
|             int tmp = screenRealSize.x; | ||||
|             screenRealSize.x = screenRealSize.y; | ||||
|             screenRealSize.y = tmp; | ||||
|         } | ||||
|  | ||||
|         // H3 resolution, any resolution smaller than that is not correctly supported | ||||
|         Point minResolution = new Point(800, 600); | ||||
|         // arbitrary limit on *downscaling*. Allow some downscaling, if requested by user. Should be generally limited to 100+ for all but few devices | ||||
|         double minimalScaling = 50; | ||||
|  | ||||
|         Point renderResolution = screenRealSize; | ||||
|         double maximalScalingWidth = 100.0 * renderResolution.x / minResolution.x; | ||||
|         double maximalScalingHeight = 100.0 * renderResolution.y / minResolution.y; | ||||
|         double maximalScaling = Math.min(maximalScalingWidth, maximalScalingHeight); | ||||
|  | ||||
|         return new int[] { (int)minimalScaling, (int)maximalScaling }; | ||||
|     } | ||||
|  | ||||
|     private static List<ScreenScaleSettingController.ScreenScale> loadScales(Activity activity) | ||||
|     { | ||||
|         List<ScreenScaleSettingController.ScreenScale> availableScales = new ArrayList<>(); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             int[] supportedScalingRange = getSupportedScalingRange(activity); | ||||
|             for (int i = 0; i <= supportedScalingRange[1] + 10 - 1; i += 10) | ||||
|             { | ||||
|                 if (i >= supportedScalingRange[0]) | ||||
|                     availableScales.add(new ScreenScaleSettingController.ScreenScale(i)); | ||||
|             } | ||||
|  | ||||
|             if(availableScales.isEmpty()) | ||||
|             { | ||||
|                 availableScales.add(new ScreenScaleSettingController.ScreenScale(100)); | ||||
|             } | ||||
|         } | ||||
|         catch(Exception ex) | ||||
|         { | ||||
|             ex.printStackTrace(); | ||||
|  | ||||
|             availableScales.clear(); | ||||
|  | ||||
|             availableScales.add(new ScreenScaleSettingController.ScreenScale(100)); | ||||
|         } | ||||
|  | ||||
|         return availableScales; | ||||
|     } | ||||
| } | ||||
| @@ -1,40 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
|  | ||||
| import eu.vcmi.vcmi.Config; | ||||
| import eu.vcmi.vcmi.R; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class SoundSettingController extends LauncherSettingWithSliderController<Integer, Config> | ||||
| { | ||||
|     public SoundSettingController(final AppCompatActivity act) | ||||
|     { | ||||
|         super(act, 0, 100); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onValueChanged(final int v) | ||||
|     { | ||||
|         mConfig.updateSound(v); | ||||
|         updateContent(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected int currentValue() | ||||
|     { | ||||
|         if (mConfig == null) | ||||
|         { | ||||
|             return Config.DEFAULT_SOUND_VALUE; | ||||
|         } | ||||
|         return mConfig.mVolumeSound; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String mainText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_sound_title); | ||||
|     } | ||||
| } | ||||
| @@ -1,39 +0,0 @@ | ||||
| package eu.vcmi.vcmi.settings; | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import android.view.View; | ||||
|  | ||||
| import eu.vcmi.vcmi.R; | ||||
| import eu.vcmi.vcmi.util.GeneratedVersion; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class StartGameController extends LauncherSettingController<Void, Void> | ||||
| { | ||||
|     private View.OnClickListener mOnSelectedAction; | ||||
|  | ||||
|     public StartGameController(final AppCompatActivity act, final View.OnClickListener onSelectedAction) | ||||
|     { | ||||
|         super(act); | ||||
|         mOnSelectedAction = onSelectedAction; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String mainText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_start_title); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected String subText() | ||||
|     { | ||||
|         return mActivity.getString(R.string.launcher_btn_start_subtitle, GeneratedVersion.VCMI_VERSION); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onClick(final View v) | ||||
|     { | ||||
|         mOnSelectedAction.onClick(v); | ||||
|     } | ||||
| } | ||||
| @@ -1,49 +0,0 @@ | ||||
| package eu.vcmi.vcmi.util; | ||||
|  | ||||
| import android.annotation.TargetApi; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.Build; | ||||
| import androidx.annotation.RequiresApi; | ||||
|  | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.URL; | ||||
| import java.util.Scanner; | ||||
|  | ||||
| import eu.vcmi.vcmi.Const; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public abstract class AsyncRequest<T> extends AsyncTask<String, Void, ServerResponse<T>> | ||||
| { | ||||
|     @TargetApi(Const.SUPPRESS_TRY_WITH_RESOURCES_WARNING) | ||||
|     protected ServerResponse<T> sendRequest(final String url) | ||||
|     { | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); | ||||
|             final int responseCode = conn.getResponseCode(); | ||||
|             if (!ServerResponse.isResponseCodeValid(responseCode)) | ||||
|             { | ||||
|                 return new ServerResponse<>(responseCode, null); | ||||
|             } | ||||
|  | ||||
|             try (Scanner s = new java.util.Scanner(conn.getInputStream()).useDelimiter("\\A")) | ||||
|             { | ||||
|                 final String response = s.hasNext() ? s.next() : ""; | ||||
|                 return new ServerResponse<>(responseCode, response); | ||||
|             } | ||||
|             catch (final Exception e) | ||||
|             { | ||||
|                 Log.e(this, "Request failed: ", e); | ||||
|             } | ||||
|         } | ||||
|         catch (final Exception e) | ||||
|         { | ||||
|             Log.e(this, "Request failed: ", e); | ||||
|         } | ||||
|         return new ServerResponse<>(ServerResponse.LOCAL_ERROR_IO, null); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,177 +1,101 @@ | ||||
| package eu.vcmi.vcmi.util; | ||||
|  | ||||
| import android.annotation.TargetApi; | ||||
| import android.content.res.AssetManager; | ||||
| import android.os.Environment; | ||||
| import android.text.TextUtils; | ||||
| import android.app.Activity; | ||||
| import android.net.Uri; | ||||
|  | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.documentfile.provider.DocumentFile; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.FileInputStream; | ||||
| import java.io.FileNotFoundException; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.FileReader; | ||||
| import java.io.FileWriter; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.io.InputStreamReader; | ||||
| import java.io.OutputStream; | ||||
| import java.nio.charset.Charset; | ||||
| import java.util.zip.ZipEntry; | ||||
| import java.util.zip.ZipFile; | ||||
| import java.util.zip.ZipInputStream; | ||||
| import java.util.List; | ||||
|  | ||||
| import eu.vcmi.vcmi.Const; | ||||
| import eu.vcmi.vcmi.Storage; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| @TargetApi(Const.SUPPRESS_TRY_WITH_RESOURCES_WARNING) | ||||
| public class FileUtil | ||||
| { | ||||
|     private static final int BUFFER_SIZE = 4096; | ||||
|  | ||||
|     public static String read(final InputStream stream) throws IOException | ||||
|     public static boolean copyData(Uri folderToCopy, Activity activity) | ||||
|     { | ||||
|         try (InputStreamReader reader = new InputStreamReader(stream)) | ||||
|         { | ||||
|             return readInternal(reader); | ||||
|         } | ||||
|         File targetDir = Storage.getVcmiDataDir(activity); | ||||
|         DocumentFile sourceDir = DocumentFile.fromTreeUri(activity, folderToCopy); | ||||
|         return copyDirectory(targetDir, sourceDir, List.of("Data", "Maps", "Mp3"), activity); | ||||
|     } | ||||
|  | ||||
|     public static String read(final File file) throws IOException | ||||
|     private static boolean copyDirectory(File targetDir, DocumentFile sourceDir, @Nullable List<String> allowed, Activity activity) | ||||
|     { | ||||
|         try (FileReader reader = new FileReader(file)) | ||||
|         if (!targetDir.exists()) | ||||
|         { | ||||
|             return readInternal(reader); | ||||
|         } | ||||
|         catch (final FileNotFoundException ignored) | ||||
|         { | ||||
|             Log.w("Could not load file: " + file); | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static String readInternal(final InputStreamReader reader) throws IOException | ||||
|     { | ||||
|         final char[] buffer = new char[BUFFER_SIZE]; | ||||
|         int currentRead; | ||||
|         final StringBuilder content = new StringBuilder(); | ||||
|         while ((currentRead = reader.read(buffer, 0, BUFFER_SIZE)) >= 0) | ||||
|         { | ||||
|             content.append(buffer, 0, currentRead); | ||||
|         } | ||||
|         return content.toString(); | ||||
|     } | ||||
|  | ||||
|     public static void write(final File file, final String data) throws IOException | ||||
|     { | ||||
|         if (!ensureWriteable(file)) | ||||
|         { | ||||
|             Log.e("Couldn't write " + data + " to " + file); | ||||
|             return; | ||||
|         } | ||||
|         try (final FileWriter fw = new FileWriter(file, false)) | ||||
|         { | ||||
|             Log.v(null, "Saving data: " + data + " to " + file.getAbsolutePath()); | ||||
|             fw.write(data); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static boolean ensureWriteable(final File file) | ||||
|     { | ||||
|         if (file == null) | ||||
|         { | ||||
|             Log.e("Broken path given to fileutil::ensureWriteable"); | ||||
|             return false; | ||||
|             targetDir.mkdir(); | ||||
|         } | ||||
|  | ||||
|         final File dir = file.getParentFile(); | ||||
|  | ||||
|         if (dir.exists() || dir.mkdirs()) | ||||
|         for (DocumentFile child : sourceDir.listFiles()) | ||||
|         { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         Log.e("Couldn't create dir " + dir); | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     public static boolean clearDirectory(final File dir) | ||||
|     { | ||||
|         if (dir == null || dir.listFiles() == null) | ||||
|         { | ||||
|             Log.e("Broken path given to fileutil::clearDirectory"); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         for (final File f : dir.listFiles()) | ||||
|         { | ||||
|             if (f.isDirectory() && !clearDirectory(f)) | ||||
|             if (allowed != null) | ||||
|             { | ||||
|                 return false; | ||||
|                 boolean fileAllowed = false; | ||||
|  | ||||
|                 for (String str : allowed) | ||||
|                 { | ||||
|                     if (str.equalsIgnoreCase(child.getName())) | ||||
|                     { | ||||
|                         fileAllowed = true; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (!fileAllowed) | ||||
|                     continue; | ||||
|             } | ||||
|  | ||||
|             if (!f.delete()) | ||||
|             File exported = new File(targetDir, child.getName()); | ||||
|  | ||||
|             if (child.isFile()) | ||||
|             { | ||||
|                 if (!exported.exists()) | ||||
|                 { | ||||
|                     try | ||||
|                     { | ||||
|                         exported.createNewFile(); | ||||
|                     } | ||||
|                     catch (IOException e) | ||||
|                     { | ||||
|                         Log.e(activity, "createNewFile failed: " + e); | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 try ( | ||||
|                     final OutputStream targetStream = new FileOutputStream(exported, false); | ||||
|                     final InputStream sourceStream = activity.getContentResolver() | ||||
|                             .openInputStream(child.getUri())) | ||||
|                 { | ||||
|                     copyStream(sourceStream, targetStream); | ||||
|                 } | ||||
|                 catch (IOException e) | ||||
|                 { | ||||
|                     Log.e(activity, "copyStream failed: " + e); | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (child.isDirectory() && !copyDirectory(exported, child, null, activity)) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public static void copyDir(final File srcFile, final File dstFile) | ||||
|     { | ||||
|         File[] files = srcFile.listFiles(); | ||||
|  | ||||
|         if(!dstFile.exists()) dstFile.mkdir(); | ||||
|  | ||||
|         if(files == null) | ||||
|             return; | ||||
|  | ||||
|         for (File child : files){ | ||||
|             File childTarget = new File(dstFile, child.getName()); | ||||
|  | ||||
|             if(child.isDirectory()){ | ||||
|                 copyDir(child, childTarget); | ||||
|             } | ||||
|             else{ | ||||
|                 copyFile(child, childTarget); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static boolean copyFile(final File srcFile, final File dstFile) | ||||
|     { | ||||
|         if (!srcFile.exists()) | ||||
|         { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         final File dstDir = dstFile.getParentFile(); | ||||
|         if (!dstDir.exists()) | ||||
|         { | ||||
|             if (!dstDir.mkdirs()) | ||||
|             { | ||||
|                 Log.w("Couldn't create dir to copy file: " + dstFile); | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         try (final FileInputStream input = new FileInputStream(srcFile); | ||||
|              final FileOutputStream output = new FileOutputStream(dstFile)) | ||||
|         { | ||||
|             copyStream(input, output); | ||||
|             return true; | ||||
|         } | ||||
|         catch (final Exception ex) | ||||
|         { | ||||
|             Log.e("Couldn't copy " + srcFile + " to " + dstFile, ex); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static void copyStream(InputStream source, OutputStream target) throws IOException | ||||
|     private static void copyStream(InputStream source, OutputStream target) throws IOException | ||||
|     { | ||||
|         final byte[] buffer = new byte[BUFFER_SIZE]; | ||||
|         int read; | ||||
| @@ -180,171 +104,4 @@ public class FileUtil | ||||
|             target.write(buffer, 0, read); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // (when internal data have changed) | ||||
|     public static boolean reloadVcmiDataToInternalDir(final File vcmiInternalDir, final AssetManager assets) | ||||
|     { | ||||
|         return clearDirectory(vcmiInternalDir) && unpackVcmiDataToInternalDir(vcmiInternalDir, assets); | ||||
|     } | ||||
|  | ||||
|     public static boolean unpackVcmiDataToInternalDir(final File vcmiInternalDir, final AssetManager assets) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             final InputStream inputStream = assets.open("internalData.zip"); | ||||
|             final boolean success = unpackZipFile(inputStream, vcmiInternalDir, null); | ||||
|             inputStream.close(); | ||||
|             return success; | ||||
|         } | ||||
|         catch (final Exception e) | ||||
|         { | ||||
|             Log.e("Couldn't extract vcmi data to internal dir", e); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static boolean unpackZipFile( | ||||
|             final File inputFile, | ||||
|             final File destDir, | ||||
|             IZipProgressReporter progressReporter) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             final InputStream inputStream = new FileInputStream(inputFile); | ||||
|             final boolean success = unpackZipFile( | ||||
|                     inputStream, | ||||
|                     destDir, | ||||
|                     progressReporter); | ||||
|  | ||||
|             inputStream.close(); | ||||
|  | ||||
|             return success; | ||||
|         } | ||||
|         catch (final Exception e) | ||||
|         { | ||||
|             Log.e("Couldn't extract file to " + destDir, e); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static int countFilesInZip(final File zipFile) | ||||
|     { | ||||
|         int totalEntries = 0; | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             final InputStream inputStream = new FileInputStream(zipFile); | ||||
|             ZipInputStream is = new ZipInputStream(inputStream); | ||||
|             ZipEntry zipEntry; | ||||
|  | ||||
|             while ((zipEntry = is.getNextEntry()) != null) | ||||
|             { | ||||
|                 totalEntries++; | ||||
|             } | ||||
|  | ||||
|             is.closeEntry(); | ||||
|             is.close(); | ||||
|             inputStream.close(); | ||||
|         } | ||||
|         catch (final Exception e) | ||||
|         { | ||||
|             Log.e("Couldn't count items in zip", e); | ||||
|         } | ||||
|  | ||||
|         return totalEntries; | ||||
|     } | ||||
|  | ||||
|     public static boolean unpackZipFile( | ||||
|         final InputStream inputStream, | ||||
|         final File destDir, | ||||
|         final IZipProgressReporter progressReporter) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             int unpackedEntries = 0; | ||||
|             final byte[] buffer = new byte[BUFFER_SIZE]; | ||||
|  | ||||
|             ZipInputStream is = new ZipInputStream(inputStream); | ||||
|             ZipEntry zipEntry; | ||||
|  | ||||
|             while ((zipEntry = is.getNextEntry()) != null) | ||||
|             { | ||||
|                 final String fileName = zipEntry.getName(); | ||||
|                 final File newFile = new File(destDir, fileName); | ||||
|  | ||||
|                 if (newFile.exists()) | ||||
|                 { | ||||
|                     Log.d("Already exists: " + newFile.getName()); | ||||
|                     continue; | ||||
|                 } | ||||
|                 else if (zipEntry.isDirectory()) | ||||
|                 { | ||||
|                     Log.v("Creating new dir: " + zipEntry); | ||||
|                     if (!newFile.mkdirs()) | ||||
|                     { | ||||
|                         Log.e("Couldn't create directory " + newFile.getAbsolutePath()); | ||||
|                         return false; | ||||
|                     } | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 final File parentFile = new File(newFile.getParent()); | ||||
|                 if (!parentFile.exists() && !parentFile.mkdirs()) | ||||
|                 { | ||||
|                     Log.e("Couldn't create directory " + parentFile.getAbsolutePath()); | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 final FileOutputStream fos = new FileOutputStream(newFile, false); | ||||
|  | ||||
|                 int currentRead; | ||||
|                 while ((currentRead = is.read(buffer)) > 0) | ||||
|                 { | ||||
|                     fos.write(buffer, 0, currentRead); | ||||
|                 } | ||||
|  | ||||
|                 fos.flush(); | ||||
|                 fos.close(); | ||||
|                 ++unpackedEntries; | ||||
|  | ||||
|                 if(progressReporter != null) | ||||
|                 { | ||||
|                     progressReporter.onUnpacked(newFile); | ||||
|                 } | ||||
|             } | ||||
|             Log.d("Unpacked data (" + unpackedEntries + " entries)"); | ||||
|  | ||||
|             is.closeEntry(); | ||||
|             is.close(); | ||||
|             return true; | ||||
|         } | ||||
|         catch (final Exception e) | ||||
|         { | ||||
|             Log.e("Couldn't extract vcmi data to " + destDir, e); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static String configFileLocation(File filesDir) | ||||
|     { | ||||
|         return filesDir + "/config/settings.json"; | ||||
|     } | ||||
|  | ||||
|     public static String readAssetsStream(final AssetManager assets, final String assetPath) | ||||
|     { | ||||
|         if (assets == null || TextUtils.isEmpty(assetPath)) | ||||
|         { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         try (java.util.Scanner s = new java.util.Scanner(assets.open(assetPath), "UTF-8").useDelimiter("\\A")) | ||||
|         { | ||||
|             return s.hasNext() ? s.next() : null; | ||||
|         } | ||||
|         catch (final IOException e) | ||||
|         { | ||||
|             Log.e("Couldn't read stream data", e); | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,8 +0,0 @@ | ||||
| package eu.vcmi.vcmi.util; | ||||
|  | ||||
| import java.io.File; | ||||
|  | ||||
| public interface IZipProgressReporter | ||||
| { | ||||
|     void onUnpacked(File newFile); | ||||
| } | ||||
| @@ -1,198 +0,0 @@ | ||||
| package eu.vcmi.vcmi.util; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.nfc.FormatException; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.Build; | ||||
| import android.util.Log; | ||||
|  | ||||
| import java.io.BufferedInputStream; | ||||
| import java.io.File; | ||||
| import java.io.FileFilter; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.InputStream; | ||||
| import java.net.URL; | ||||
| import java.net.URLConnection; | ||||
|  | ||||
| public class InstallModAsync | ||||
|         extends AsyncTask<String, String, Boolean> | ||||
|         implements IZipProgressReporter | ||||
| { | ||||
|     private static final String TAG = "DOWNLOADFILE"; | ||||
|     private static final int DOWNLOAD_PERCENT = 70; | ||||
|  | ||||
|     private PostDownload callback; | ||||
|     private File downloadLocation; | ||||
|     private File extractLocation; | ||||
|     private Context context; | ||||
|     private int totalFiles; | ||||
|     private int unpackedFiles; | ||||
|  | ||||
|     public InstallModAsync(File extractLocation, Context context, PostDownload callback) | ||||
|     { | ||||
|         this.context = context; | ||||
|         this.callback = callback; | ||||
|         this.extractLocation = extractLocation; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected Boolean doInBackground(String... args) | ||||
|     { | ||||
|         int count; | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             File modsFolder = extractLocation.getParentFile(); | ||||
|  | ||||
|             if (!modsFolder.exists()) modsFolder.mkdir(); | ||||
|  | ||||
|             this.downloadLocation = File.createTempFile("tmp", ".zip", modsFolder); | ||||
|  | ||||
|             URL url = new URL(args[0]); | ||||
|             URLConnection connection = url.openConnection(); | ||||
|             connection.connect(); | ||||
|  | ||||
|             long lengthOfFile = -1; | ||||
|  | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) | ||||
|             { | ||||
|                 lengthOfFile = connection.getContentLengthLong(); | ||||
|             } | ||||
|  | ||||
|             if(lengthOfFile == -1) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     lengthOfFile = Long.parseLong(connection.getHeaderField("Content-Length")); | ||||
|                     Log.d(TAG, "Length of the file: " + lengthOfFile); | ||||
|                 } catch (NumberFormatException ex) | ||||
|                 { | ||||
|                     Log.d(TAG, "Failed to parse content length", ex); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if(lengthOfFile == -1) | ||||
|             { | ||||
|                 lengthOfFile = 100000000; | ||||
|                 Log.d(TAG, "Using dummy length of file"); | ||||
|             } | ||||
|  | ||||
|             InputStream input = new BufferedInputStream(url.openStream()); | ||||
|             FileOutputStream output = new FileOutputStream(downloadLocation); //context.openFileOutput("content.zip", Context.MODE_PRIVATE); | ||||
|             Log.d(TAG, "file saved at " + downloadLocation.getAbsolutePath()); | ||||
|  | ||||
|             byte data[] = new byte[1024]; | ||||
|             long total = 0; | ||||
|             while ((count = input.read(data)) != -1) | ||||
|             { | ||||
|                 total += count; | ||||
|                 output.write(data, 0, count); | ||||
|                 this.publishProgress((int) ((total * DOWNLOAD_PERCENT) / lengthOfFile) + "%"); | ||||
|             } | ||||
|  | ||||
|             output.flush(); | ||||
|             output.close(); | ||||
|             input.close(); | ||||
|  | ||||
|             File tempDir = File.createTempFile("tmp", "", modsFolder); | ||||
|  | ||||
|             tempDir.delete(); | ||||
|             tempDir.mkdir(); | ||||
|  | ||||
|             if (!extractLocation.exists()) extractLocation.mkdir(); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 totalFiles = FileUtil.countFilesInZip(downloadLocation); | ||||
|                 unpackedFiles = 0; | ||||
|  | ||||
|                 FileUtil.unpackZipFile(downloadLocation, tempDir, this); | ||||
|  | ||||
|                 return moveModToExtractLocation(tempDir); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 downloadLocation.delete(); | ||||
|                 FileUtil.clearDirectory(tempDir); | ||||
|                 tempDir.delete(); | ||||
|             } | ||||
|         } catch (Exception e) | ||||
|         { | ||||
|             Log.e(TAG, "Unhandled exception while installing mod", e); | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onProgressUpdate(String... values) | ||||
|     { | ||||
|         callback.downloadProgress(values); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void onPostExecute(Boolean result) | ||||
|     { | ||||
|         if (callback != null) callback.downloadDone(result, extractLocation); | ||||
|     } | ||||
|  | ||||
|     private boolean moveModToExtractLocation(File tempDir) | ||||
|     { | ||||
|         return moveModToExtractLocation(tempDir, 0); | ||||
|     } | ||||
|  | ||||
|     private boolean moveModToExtractLocation(File tempDir, int level) | ||||
|     { | ||||
|         File[] modJson = tempDir.listFiles(new FileFilter() | ||||
|         { | ||||
|             @Override | ||||
|             public boolean accept(File file) | ||||
|             { | ||||
|                 return file.getName().equalsIgnoreCase("Mod.json"); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         if (modJson != null && modJson.length > 0) | ||||
|         { | ||||
|             File modFolder = modJson[0].getParentFile(); | ||||
|  | ||||
|             if (!modFolder.renameTo(extractLocation)) | ||||
|             { | ||||
|                 FileUtil.copyDir(modFolder, extractLocation); | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         if (level <= 1) | ||||
|         { | ||||
|             for (File child : tempDir.listFiles()) | ||||
|             { | ||||
|                 if (child.isDirectory() && moveModToExtractLocation(child, level + 1)) | ||||
|                 { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onUnpacked(File newFile) | ||||
|     { | ||||
|         unpackedFiles++; | ||||
|  | ||||
|         int progress = DOWNLOAD_PERCENT | ||||
|                 + (unpackedFiles * (100 - DOWNLOAD_PERCENT) / totalFiles); | ||||
|  | ||||
|         publishProgress(progress + "%"); | ||||
|     } | ||||
|  | ||||
|     public interface PostDownload | ||||
|     { | ||||
|         void downloadDone(Boolean succeed, File modFolder); | ||||
|  | ||||
|         void downloadProgress(String... progress); | ||||
|     } | ||||
| } | ||||
| @@ -12,9 +12,12 @@ import eu.vcmi.vcmi.NativeMethods; | ||||
|  */ | ||||
| public final class LibsLoader | ||||
| { | ||||
|     public static final String CLIENT_LIB = "vcmiclient_" | ||||
|         + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_ABIS[0] : Build.CPU_ABI); | ||||
|  | ||||
|     public static void loadClientLibs(Context ctx) | ||||
|     { | ||||
|         SDL.loadLibrary("vcmiclient"); | ||||
|         SDL.loadLibrary(CLIENT_LIB); | ||||
|         SDL.setContext(ctx); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,15 +1,6 @@ | ||||
| package eu.vcmi.vcmi.util; | ||||
|  | ||||
| import android.os.Environment; | ||||
|  | ||||
| import java.io.BufferedWriter; | ||||
| import java.io.FileWriter; | ||||
| import java.io.IOException; | ||||
| import java.text.DateFormat; | ||||
| import java.util.Date; | ||||
|  | ||||
| import eu.vcmi.vcmi.BuildConfig; | ||||
| import eu.vcmi.vcmi.Const; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
| @@ -18,8 +9,6 @@ import eu.vcmi.vcmi.Const; | ||||
| public class Log | ||||
| { | ||||
|     private static final boolean LOGGING_ENABLED_CONSOLE = BuildConfig.DEBUG; | ||||
|     private static final boolean LOGGING_ENABLED_FILE = true; | ||||
|     private static final String FILELOG_PATH = "/" + Const.VCMI_DATA_ROOT_FOLDER_NAME + "/cache/VCMI_launcher.log"; | ||||
|     private static final String TAG_PREFIX = "VCMI/"; | ||||
|     private static final String STATIC_TAG = "static"; | ||||
|  | ||||
| @@ -34,19 +23,6 @@ public class Log | ||||
|         { | ||||
|             android.util.Log.println(priority, TAG_PREFIX + tagString, msg); | ||||
|         } | ||||
|         if (LOGGING_ENABLED_FILE) // this is probably very inefficient, but should be enough for now... | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 final BufferedWriter fileWriter = new BufferedWriter(new FileWriter(Environment.getExternalStorageDirectory() + FILELOG_PATH, true)); | ||||
|                 fileWriter.write(String.format("[%s] %s: %s\n", formatPriority(priority), tagString, msg)); | ||||
|                 fileWriter.flush(); | ||||
|                 fileWriter.close(); | ||||
|             } | ||||
|             catch (IOException ignored) | ||||
|             { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static String formatPriority(final int priority) | ||||
| @@ -77,23 +53,6 @@ public class Log | ||||
|         return obj.getClass().getSimpleName(); | ||||
|     } | ||||
|  | ||||
|     public static void init() | ||||
|     { | ||||
|         if (LOGGING_ENABLED_FILE) // clear previous log | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 final BufferedWriter fileWriter = new BufferedWriter(new FileWriter(Environment.getExternalStorageDirectory() + FILELOG_PATH, false)); | ||||
|                 fileWriter.write("Starting VCMI launcher log, " + DateFormat.getDateTimeInstance().format(new Date()) + "\n"); | ||||
|                 fileWriter.flush(); | ||||
|                 fileWriter.close(); | ||||
|             } | ||||
|             catch (IOException ignored) | ||||
|             { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static void v(final String msg) | ||||
|     { | ||||
|         logInternal(android.util.Log.VERBOSE, STATIC_TAG, msg); | ||||
|   | ||||
| @@ -1,30 +0,0 @@ | ||||
| package eu.vcmi.vcmi.util; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ServerResponse<T> | ||||
| { | ||||
|     public static final int LOCAL_ERROR_IO = -1; | ||||
|     public static final int LOCAL_ERROR_PARSING = -2; | ||||
|  | ||||
|     public int mCode; | ||||
|     public String mRawContent; | ||||
|     public T mContent; | ||||
|  | ||||
|     public ServerResponse(final int code, final String content) | ||||
|     { | ||||
|         mCode = code; | ||||
|         mRawContent = content; | ||||
|     } | ||||
|  | ||||
|     public static boolean isResponseCodeValid(final int responseCode) | ||||
|     { | ||||
|         return responseCode >= 200 && responseCode < 300; | ||||
|     } | ||||
|  | ||||
|     public boolean isValid() | ||||
|     { | ||||
|         return isResponseCodeValid(mCode); | ||||
|     } | ||||
| } | ||||
| @@ -1,92 +0,0 @@ | ||||
| package eu.vcmi.vcmi.util; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import androidx.annotation.NonNull; | ||||
|  | ||||
| /** | ||||
|  * simple shared preferences wrapper | ||||
|  * | ||||
|  * @author F | ||||
|  */ | ||||
| public class SharedPrefs | ||||
| { | ||||
|     public static final String KEY_CURRENT_INTERNAL_ASSET_HASH = "KEY_CURRENT_INTERNAL_ASSET_HASH"; // [string] | ||||
|     private static final String VCMI_PREFS_NAME = "VCMIPrefs"; | ||||
|     private final SharedPreferences mPrefs; | ||||
|  | ||||
|     public SharedPrefs(final Context ctx) | ||||
|     { | ||||
|         mPrefs = ctx.getSharedPreferences(VCMI_PREFS_NAME, Context.MODE_PRIVATE); | ||||
|     } | ||||
|  | ||||
|     public void save(final String name, final String value) | ||||
|     { | ||||
|         mPrefs.edit().putString(name, value).apply(); | ||||
|         log(name, value, true); | ||||
|     } | ||||
|  | ||||
|     public String load(final String name, final String defaultValue) | ||||
|     { | ||||
|         return log(name, mPrefs.getString(name, defaultValue), false); | ||||
|     } | ||||
|  | ||||
|     public void save(final String name, final int value) | ||||
|     { | ||||
|         mPrefs.edit().putInt(name, value).apply(); | ||||
|         log(name, value, true); | ||||
|     } | ||||
|  | ||||
|     public int load(final String name, final int defaultValue) | ||||
|     { | ||||
|         return log(name, mPrefs.getInt(name, defaultValue), false); | ||||
|     } | ||||
|  | ||||
|     public void save(final String name, final float value) | ||||
|     { | ||||
|         mPrefs.edit().putFloat(name, value).apply(); | ||||
|         log(name, value, true); | ||||
|     } | ||||
|  | ||||
|     public float load(final String name, final float defaultValue) | ||||
|     { | ||||
|         return log(name, mPrefs.getFloat(name, defaultValue), false); | ||||
|     } | ||||
|  | ||||
|     public void save(final String name, final boolean value) | ||||
|     { | ||||
|         mPrefs.edit().putBoolean(name, value).apply(); | ||||
|         log(name, value, true); | ||||
|     } | ||||
|  | ||||
|     public boolean load(final String name, final boolean defaultValue) | ||||
|     { | ||||
|         return log(name, mPrefs.getBoolean(name, defaultValue), false); | ||||
|     } | ||||
|  | ||||
|     public <T extends Enum<T>> void saveEnum(final String name, final T value) | ||||
|     { | ||||
|         mPrefs.edit().putInt(name, value.ordinal()).apply(); | ||||
|         log(name, value, true); | ||||
|     } | ||||
|  | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public <T extends Enum<T>> T loadEnum(final String name, @NonNull final T defaultValue) | ||||
|     { | ||||
|         final int rawValue = mPrefs.getInt(name, defaultValue.ordinal()); | ||||
|         return (T) log(name, defaultValue.getClass().getEnumConstants()[rawValue], false); | ||||
|     } | ||||
|  | ||||
|     private <T> T log(final String key, final T value, final boolean saving) | ||||
|     { | ||||
|         if (saving) | ||||
|         { | ||||
|             Log.v(this, "[prefs saving] " + key + " => " + value); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             Log.v(this, "[prefs loading] " + key + " => " + value); | ||||
|         } | ||||
|         return value; | ||||
|     } | ||||
| } | ||||
| @@ -1,58 +0,0 @@ | ||||
| package eu.vcmi.vcmi.util; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.pm.PackageInfo; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.content.res.Resources; | ||||
| import android.util.DisplayMetrics; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
|  | ||||
| public final class Utils | ||||
| { | ||||
|     private static String sAppVersionCache; | ||||
|  | ||||
|     private Utils() | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     public static String appVersionName(final Context ctx) | ||||
|     { | ||||
|         if (sAppVersionCache == null) | ||||
|         { | ||||
|             final PackageManager pm = ctx.getPackageManager(); | ||||
|             try | ||||
|             { | ||||
|                 final PackageInfo info = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_META_DATA); | ||||
|                 return sAppVersionCache = info.versionName; | ||||
|             } | ||||
|             catch (final PackageManager.NameNotFoundException e) | ||||
|             { | ||||
|                 Log.e(ctx, "Couldn't resolve app version", e); | ||||
|             } | ||||
|         } | ||||
|         return sAppVersionCache; | ||||
|     } | ||||
|  | ||||
|     public static float convertDpToPx(final Context ctx, final float dp) | ||||
|     { | ||||
|         return convertDpToPx(ctx.getResources(), dp); | ||||
|     } | ||||
|  | ||||
|     public static float convertDpToPx(final Resources res, final float dp) | ||||
|     { | ||||
|         return dp * res.getDisplayMetrics().density; | ||||
|     } | ||||
|  | ||||
|     public static float convertPxToDp(final Context ctx, final float px) | ||||
|     { | ||||
|         return convertPxToDp(ctx.getResources(), px); | ||||
|     } | ||||
|  | ||||
|     public static float convertPxToDp(final Resources res, final float px) | ||||
|     { | ||||
|         return px / res.getDisplayMetrics().density; | ||||
|     } | ||||
| } | ||||
| @@ -1,52 +0,0 @@ | ||||
| package eu.vcmi.vcmi.viewmodels; | ||||
|  | ||||
| import android.view.View; | ||||
|  | ||||
| import androidx.lifecycle.ViewModel; | ||||
| import androidx.databinding.PropertyChangeRegistry; | ||||
| import androidx.databinding.Observable; | ||||
|  | ||||
| /** | ||||
|  * @author F | ||||
|  */ | ||||
| public class ObservableViewModel extends ViewModel implements Observable | ||||
| { | ||||
|     private PropertyChangeRegistry callbacks = new PropertyChangeRegistry(); | ||||
|  | ||||
|     @Override | ||||
|     public void addOnPropertyChangedCallback( | ||||
|             Observable.OnPropertyChangedCallback callback) | ||||
|     { | ||||
|         callbacks.add(callback); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void removeOnPropertyChangedCallback( | ||||
|             Observable.OnPropertyChangedCallback callback) | ||||
|     { | ||||
|         callbacks.remove(callback); | ||||
|     } | ||||
|  | ||||
|     public int visible(boolean isVisible) | ||||
|     { | ||||
|         return isVisible ? View.VISIBLE : View.GONE; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Notifies observers that all properties of this instance have changed. | ||||
|      */ | ||||
|     void notifyChange() { | ||||
|         callbacks.notifyCallbacks(this, 0, null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Notifies observers that a specific property has changed. The getter for the | ||||
|      * property that changes should be marked with the @Bindable annotation to | ||||
|      * generate a field in the BR class to be used as the fieldId parameter. | ||||
|      * | ||||
|      * @param fieldId The generated BR id for the Bindable field. | ||||
|      */ | ||||
|     void notifyPropertyChanged(int fieldId) { | ||||
|         callbacks.notifyCallbacks(this, fieldId, null); | ||||
|     } | ||||
| } | ||||
| @@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh | ||||
|     private static final String TAG = "SDL"; | ||||
|     private static final int SDL_MAJOR_VERSION = 2; | ||||
|     private static final int SDL_MINOR_VERSION = 26; | ||||
|     private static final int SDL_MICRO_VERSION = 1; | ||||
|     private static final int SDL_MICRO_VERSION = 5; | ||||
| /* | ||||
|     // Display InputType.SOURCE/CLASS of events and devices | ||||
|     // | ||||
| @@ -241,7 +241,14 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh | ||||
|      * It can be overridden by derived classes. | ||||
|      */ | ||||
|     protected String getMainSharedObject() { | ||||
|         return null; | ||||
|         String library; | ||||
|         String[] libraries = SDLActivity.mSingleton.getLibraries(); | ||||
|         if (libraries.length > 0) { | ||||
|             library = "lib" + libraries[libraries.length - 1] + ".so"; | ||||
|         } else { | ||||
|             library = "libmain.so"; | ||||
|         } | ||||
|         return getContext().getApplicationInfo().nativeLibraryDir + "/" + library; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -168,6 +168,32 @@ class SDLJoystickHandler_API16 extends SDLJoystickHandler { | ||||
|                 arg1Axis = MotionEvent.AXIS_GAS; | ||||
|             } | ||||
|  | ||||
|             // Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ. | ||||
|             // This is because the usual pairing are: | ||||
|             // - AXIS_X + AXIS_Y (left stick). | ||||
|             // - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers). | ||||
|             // - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers). | ||||
|             // This sorts the axes in the above order, which tends to be correct | ||||
|             // for Xbox-ish game pads that have the right stick on RX/RY and the | ||||
|             // triggers on Z/RZ. | ||||
|             // | ||||
|             // Gamepads that don't have AXIS_Z/AXIS_RZ but use | ||||
|             // AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this. | ||||
|             // | ||||
|             // References: | ||||
|             // - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input | ||||
|             // - https://www.kernel.org/doc/html/latest/input/gamepad.html | ||||
|             if (arg0Axis == MotionEvent.AXIS_Z) { | ||||
|                 arg0Axis = MotionEvent.AXIS_RZ - 1; | ||||
|             } else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) { | ||||
|                 --arg0Axis; | ||||
|             } | ||||
|             if (arg1Axis == MotionEvent.AXIS_Z) { | ||||
|                 arg1Axis = MotionEvent.AXIS_RZ - 1; | ||||
|             } else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) { | ||||
|                 --arg1Axis; | ||||
|             } | ||||
|  | ||||
|             return arg0Axis - arg1Axis; | ||||
|         } | ||||
|     } | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 156 B | 
| @@ -1,30 +0,0 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:aapt="http://schemas.android.com/aapt" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportHeight="108" | ||||
|     android:viewportWidth="108"> | ||||
|     <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> | ||||
|         <aapt:attr name="android:fillColor"> | ||||
|             <gradient | ||||
|                 android:endX="85.84757" | ||||
|                 android:endY="92.4963" | ||||
|                 android:startX="42.9492" | ||||
|                 android:startY="49.59793" | ||||
|                 android:type="linear"> | ||||
|                 <item | ||||
|                     android:color="#44000000" | ||||
|                     android:offset="0.0" /> | ||||
|                 <item | ||||
|                     android:color="#00000000" | ||||
|                     android:offset="1.0" /> | ||||
|             </gradient> | ||||
|         </aapt:attr> | ||||
|     </path> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFF" | ||||
|         android:fillType="nonZero" | ||||
|         android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" | ||||
|         android:strokeColor="#00000000" | ||||
|         android:strokeWidth="1" /> | ||||
| </vector> | ||||
| @@ -1,4 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <shape xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <gradient android:startColor="#000000" android:endColor="#00000000" android:angle="270"/> | ||||
| </shape> | ||||
| @@ -1,9 +0,0 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="24dp" | ||||
|     android:height="24dp" | ||||
|     android:viewportHeight="24.0" | ||||
|     android:viewportWidth="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFFFF" | ||||
|         android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" /> | ||||
| </vector> | ||||
| @@ -1,9 +0,0 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="24dp" | ||||
|     android:height="24dp" | ||||
|     android:viewportHeight="24.0" | ||||
|     android:viewportWidth="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFFFF" | ||||
|         android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z" /> | ||||
| </vector> | ||||
| @@ -1,170 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportHeight="108" | ||||
|     android:viewportWidth="108"> | ||||
|     <path | ||||
|         android:fillColor="#3DDC84" | ||||
|         android:pathData="M0,0h108v108h-108z" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M9,0L9,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,0L19,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M29,0L29,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M39,0L39,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M49,0L49,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M59,0L59,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M69,0L69,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M79,0L79,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M89,0L89,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M99,0L99,108" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,9L108,9" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,19L108,19" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,29L108,29" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,39L108,39" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,49L108,49" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,59L108,59" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,69L108,69" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,79L108,79" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,89L108,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M0,99L108,99" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,29L89,29" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,39L89,39" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,49L89,49" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,59L89,59" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,69L89,69" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M19,79L89,79" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M29,19L29,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M39,19L39,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M49,19L49,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M59,19L59,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M69,19L69,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
|     <path | ||||
|         android:fillColor="#00000000" | ||||
|         android:pathData="M79,19L79,89" | ||||
|         android:strokeColor="#33FFFFFF" | ||||
|         android:strokeWidth="0.8" /> | ||||
| </vector> | ||||
| @@ -1,9 +0,0 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="40dp" | ||||
|     android:height="40dp" | ||||
|     android:viewportHeight="24.0" | ||||
|     android:viewportWidth="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFFFF" | ||||
|         android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" /> | ||||
| </vector> | ||||
| @@ -1,9 +0,0 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="40dp" | ||||
|     android:height="40dp" | ||||
|     android:viewportHeight="24.0" | ||||
|     android:viewportWidth="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFFFF" | ||||
|         android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" /> | ||||
| </vector> | ||||
| @@ -1,9 +0,0 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="40dp" | ||||
|     android:height="40dp" | ||||
|     android:viewportHeight="24.0" | ||||
|     android:viewportWidth="24.0"> | ||||
|     <path | ||||
|         android:fillColor="#FFFFFFFF" | ||||
|         android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4V6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" /> | ||||
| </vector> | ||||
| @@ -1,6 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <shape xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <corners android:radius="4dp" /> | ||||
|     <solid android:color="#A0000000" /> | ||||
|     <stroke android:color="@color/accent" android:width="1dp" /> | ||||
| </shape> | ||||
| @@ -1,7 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <shape xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <size | ||||
|         android:width="1px" | ||||
|         android:height="1px" /> | ||||
|     <solid android:color="@color/accent" /> | ||||
| </shape> | ||||
| @@ -1,19 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <layout> | ||||
|     <com.google.android.material.appbar.AppBarLayout | ||||
|         xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="?attr/actionBarSize" | ||||
|         android:background="@color/bgMain"> | ||||
|  | ||||
|         <androidx.appcompat.widget.Toolbar | ||||
|             android:id="@+id/toolbar" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:background="@color/bgMain" | ||||
|             android:elevation="6dp" | ||||
|             app:elevation="6dp" | ||||
|             app:title="@string/launcher_title" /> | ||||
|     </com.google.android.material.appbar.AppBarLayout> | ||||
| </layout> | ||||
| @@ -1,71 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <ScrollView | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:layout_below="@+id/toolbar_include"> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:orientation="vertical"> | ||||
|  | ||||
|         <LinearLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:orientation="vertical" | ||||
|             android:padding="@dimen/side_margin"> | ||||
|  | ||||
|             <androidx.appcompat.widget.AppCompatTextView | ||||
|                 style="@style/VCMI.Text.Header" | ||||
|                 android:text="@string/app_name" /> | ||||
|  | ||||
|             <androidx.appcompat.widget.AppCompatTextView | ||||
|                 android:id="@+id/about_version_app" | ||||
|                 style="@style/VCMI.Text" | ||||
|                 android:text="@string/about_version_app" /> | ||||
|  | ||||
|             <androidx.appcompat.widget.AppCompatTextView | ||||
|                 android:id="@+id/about_version_launcher" | ||||
|                 style="@style/VCMI.Text" | ||||
|                 android:text="@string/about_version_launcher" /> | ||||
|         </LinearLayout> | ||||
|  | ||||
|         <include layout="@layout/inc_separator" /> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             style="@style/VCMI.Text.LauncherSection" | ||||
|             android:text="@string/about_section_project" /> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             android:id="@+id/about_link_portal" | ||||
|             style="@style/VCMI.Entry.Clickable.AboutSimpleEntry" | ||||
|             android:text="@string/about_links_main" /> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             android:id="@+id/about_link_repo_main" | ||||
|             style="@style/VCMI.Entry.Clickable.AboutSimpleEntry" | ||||
|             android:text="@string/about_links_repo" /> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             android:id="@+id/about_link_repo_launcher" | ||||
|             style="@style/VCMI.Entry.Clickable.AboutSimpleEntry" | ||||
|             android:text="@string/about_links_repo_launcher" /> | ||||
|  | ||||
|         <include layout="@layout/inc_separator" /> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             style="@style/VCMI.Text.LauncherSection" | ||||
|             android:text="@string/about_section_legal" /> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             android:id="@+id/about_btn_authors" | ||||
|             style="@style/VCMI.Entry.Clickable.AboutSimpleEntry" | ||||
|             android:text="@string/about_btn_authors" /> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             android:id="@+id/about_btn_privacy" | ||||
|             style="@style/VCMI.Entry.Clickable.AboutSimpleEntry" | ||||
|             android:text="@string/about_btn_privacy" /> | ||||
|     </LinearLayout> | ||||
| </ScrollView> | ||||
| @@ -1,22 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical"> | ||||
|  | ||||
|     <androidx.appcompat.widget.AppCompatTextView | ||||
|         android:id="@+id/error_message" | ||||
|         style="@style/VCMI.Text" | ||||
|         android:layout_margin="@dimen/side_margin" | ||||
|         android:text="@string/app_name" /> | ||||
|  | ||||
|     <androidx.appcompat.widget.AppCompatButton | ||||
|         android:id="@+id/error_btn_try_again" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_gravity="right" | ||||
|         android:layout_margin="@dimen/side_margin" | ||||
|         android:layout_marginTop="20dp" | ||||
|         android:text="@string/misc_try_again" /> | ||||
| </LinearLayout> | ||||
| @@ -16,4 +16,4 @@ | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_gravity="center" | ||||
|         android:visibility="gone" /> | ||||
| </FrameLayout> | ||||
| </FrameLayout> | ||||
|   | ||||
| @@ -1,106 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <ScrollView | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:layout_below="@+id/toolbar_include" | ||||
|     android:clipChildren="false" | ||||
|     android:clipToPadding="false"> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginTop="10dp" | ||||
|         android:orientation="vertical"> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             android:id="@+id/launcher_version_info" | ||||
|             style="@style/VCMI.Text" | ||||
|             android:padding="@dimen/side_margin" | ||||
|             android:text="@string/app_name" /> | ||||
|  | ||||
|         <include layout="@layout/inc_separator" /> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             style="@style/VCMI.Text.LauncherSection" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:elevation="2dp" | ||||
|             android:text="@string/launcher_section_init" | ||||
|             app:elevation="2dp" /> | ||||
|  | ||||
|         <FrameLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content"> | ||||
|  | ||||
|             <include | ||||
|                 android:id="@+id/launcher_btn_start" | ||||
|                 layout="@layout/inc_launcher_btn" /> | ||||
|  | ||||
|             <androidx.appcompat.widget.AppCompatTextView | ||||
|                 android:id="@+id/launcher_error" | ||||
|                 style="@style/VCMI.Text" | ||||
|                 android:drawableLeft="@drawable/ic_error" | ||||
|                 android:drawablePadding="10dp" | ||||
|                 android:gravity="center_vertical" | ||||
|                 android:minHeight="80dp" | ||||
|                 android:padding="@dimen/side_margin" | ||||
|                 android:text="@string/app_name" /> | ||||
|  | ||||
|             <ProgressBar | ||||
|                 android:id="@+id/launcher_progress" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_gravity="center" /> | ||||
|         </FrameLayout> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/launcher_btn_copy" | ||||
|             layout="@layout/inc_launcher_btn" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/launcher_btn_export" | ||||
|             layout="@layout/inc_launcher_btn" /> | ||||
|  | ||||
|         <include layout="@layout/inc_separator" /> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             style="@style/VCMI.Text.LauncherSection" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:text="@string/launcher_section_settings" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/launcher_btn_mods" | ||||
|             layout="@layout/inc_launcher_btn" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/launcher_btn_scale" | ||||
|             layout="@layout/inc_launcher_btn" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/launcher_btn_adventure_ai" | ||||
|             layout="@layout/inc_launcher_btn" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/launcher_btn_cp" | ||||
|             layout="@layout/inc_launcher_btn" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/launcher_btn_pointer_mode" | ||||
|             layout="@layout/inc_launcher_btn" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/launcher_btn_pointer_multi" | ||||
|             layout="@layout/inc_launcher_btn" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/launcher_btn_volume_sound" | ||||
|             layout="@layout/inc_launcher_slider" /> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/launcher_btn_volume_music" | ||||
|             layout="@layout/inc_launcher_slider" /> | ||||
|     </LinearLayout> | ||||
| </ScrollView> | ||||
| @@ -1,29 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <FrameLayout | ||||
|     android:id="@+id/mods_data_root" | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:layout_below="@+id/toolbar_include" | ||||
|     android:clipChildren="false" | ||||
|     android:clipToPadding="false"> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/mods_recycler" | ||||
|         xmlns:tools="http://schemas.android.com/tools" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         tools:listitem="@layout/mods_adapter_item" /> | ||||
|  | ||||
|     <TextView | ||||
|         android:id="@+id/mods_error_text" | ||||
|         style="@style/VCMI.Text" | ||||
|         android:layout_marginTop="30dp" | ||||
|         android:gravity="center" /> | ||||
|  | ||||
|     <ProgressBar | ||||
|         android:id="@+id/mods_progress" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_gravity="center" /> | ||||
| </FrameLayout> | ||||
| @@ -1,18 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <layout xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <RelativeLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent"> | ||||
|  | ||||
|         <include | ||||
|             android:id="@+id/toolbar_include" | ||||
|             layout="@layout/inc_toolbar" /> | ||||
|  | ||||
|         <ViewStub | ||||
|             android:id="@+id/toolbar_wrapper_content_stub" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="match_parent" | ||||
|             android:layout_below="@id/toolbar_include" /> | ||||
|  | ||||
|     </RelativeLayout> | ||||
| </layout> | ||||
| @@ -1,34 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <ScrollView | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:orientation="vertical"> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             style="@style/VCMI.Text.LauncherSection" | ||||
|             android:text="@string/dialog_authors_vcmi" /> | ||||
|  | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             android:id="@+id/dialog_authors_vcmi" | ||||
|             style="@style/VCMI.Text" | ||||
|             android:padding="@dimen/side_margin" /> | ||||
|  | ||||
|         <include layout="@layout/inc_separator" /> | ||||
|  | ||||
|         <!-- TODO should this be separate or just merged with vcmi authors? --> | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             style="@style/VCMI.Text.LauncherSection" | ||||
|             android:text="@string/dialog_authors_launcher" /> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             android:id="@+id/dialog_authors_launcher" | ||||
|             style="@style/VCMI.Text" | ||||
|             android:padding="@dimen/side_margin" /> | ||||
|     </LinearLayout> | ||||
| </ScrollView> | ||||
| @@ -1,33 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <layout xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <data> | ||||
|         <variable | ||||
|             name="title" | ||||
|             type="java.lang.String" /> | ||||
|         <variable | ||||
|             name="description" | ||||
|             type="java.lang.String" /> | ||||
|     </data> | ||||
|     <RelativeLayout | ||||
|         style="@style/VCMI.Entry.Clickable" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content"> | ||||
|  | ||||
|         <LinearLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_centerVertical="true" | ||||
|             android:orientation="vertical"> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/inc_launcher_btn_main" | ||||
|                 style="@style/VCMI.Text.LauncherEntry" | ||||
|                 android:text="@{title}" /> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/inc_launcher_btn_sub" | ||||
|                 style="@style/VCMI.Text.LauncherEntry.Sub" | ||||
|                 android:text="@{description}" /> | ||||
|         </LinearLayout> | ||||
|     </RelativeLayout> | ||||
| </layout> | ||||
| @@ -1,27 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <layout xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <RelativeLayout | ||||
|         style="@style/VCMI.Entry" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content"> | ||||
|  | ||||
|         <LinearLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_centerVertical="true" | ||||
|             android:orientation="vertical"> | ||||
|  | ||||
|             <TextView | ||||
|                 android:id="@+id/inc_launcher_btn_main" | ||||
|                 style="@style/VCMI.Text.LauncherEntry" | ||||
|                 android:text="@string/app_name" /> | ||||
|  | ||||
|             <androidx.appcompat.widget.AppCompatSeekBar | ||||
|                 android:id="@+id/inc_launcher_btn_slider" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:paddingTop="8dp" | ||||
|                 android:paddingBottom="8dp" /> | ||||
|         </LinearLayout> | ||||
|     </RelativeLayout> | ||||
| </layout> | ||||
| @@ -1,7 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <layout xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <View | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="1px" | ||||
|         android:background="@color/separator" /> | ||||
| </layout> | ||||
| @@ -1,28 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <layout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
|  | ||||
|     <RelativeLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="?attr/actionBarSize" | ||||
|         android:clipChildren="false" | ||||
|         android:clipToPadding="false"> | ||||
|  | ||||
|         <androidx.appcompat.widget.Toolbar | ||||
|             android:id="@+id/toolbar" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="?attr/actionBarSize" | ||||
|             android:background="@color/bgMain" | ||||
|             android:elevation="6dp" | ||||
|             app:elevation="6dp" | ||||
|             app:title="@string/launcher_title" /> | ||||
|  | ||||
|         <ImageView | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="4dp" | ||||
|             android:layout_alignParentBottom="true" | ||||
|             android:layout_marginBottom="-4dp" | ||||
|             android:background="@drawable/compat_toolbar_shadow" /> | ||||
|     </RelativeLayout> | ||||
| </layout> | ||||
| @@ -1,25 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_gravity="center" | ||||
|     android:orientation="horizontal"> | ||||
|  | ||||
|     <View | ||||
|         android:id="@+id/mods_adapter_item_nesting" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="match_parent" | ||||
|         android:background="@color/accent" /> | ||||
|  | ||||
|     <androidx.appcompat.widget.AppCompatTextView | ||||
|         android:id="@+id/mods_adapter_item_name" | ||||
|         style="@style/VCMI.Text" | ||||
|         xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:layout_marginLeft="8dp" | ||||
|         android:drawableLeft="@drawable/ic_error" | ||||
|         android:drawablePadding="18dp" | ||||
|         android:gravity="center_vertical" | ||||
|         android:padding="@dimen/side_margin" | ||||
|         android:text="@string/mods_failed_mod_loading" /> | ||||
| </LinearLayout> | ||||
| @@ -1,101 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_gravity="center" | ||||
|     android:background="?attr/selectableItemBackground" | ||||
|     android:minHeight="@dimen/entry_min_height" | ||||
|     android:orientation="horizontal"> | ||||
|  | ||||
|     <View | ||||
|         android:id="@+id/mods_adapter_item_nesting" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="match_parent" | ||||
|         android:background="@color/accent" /> | ||||
|  | ||||
|     <LinearLayout | ||||
|         style="@style/VCMI.Entry" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_gravity="center_vertical" | ||||
|         android:elevation="4dp" | ||||
|         android:gravity="center_vertical" | ||||
|         android:orientation="horizontal" | ||||
|         android:paddingBottom="5dp" | ||||
|         android:paddingLeft="@dimen/side_margin" | ||||
|         android:paddingTop="5dp"> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatImageView | ||||
|             android:id="@+id/mods_adapter_item_status" | ||||
|             android:layout_width="40dp" | ||||
|             android:layout_height="40dp" | ||||
|             android:layout_marginRight="10dp" | ||||
|             android:src="@drawable/ic_star_full" /> | ||||
|  | ||||
|         <LinearLayout | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginRight="10dp" | ||||
|             android:layout_weight="1" | ||||
|             android:orientation="vertical"> | ||||
|  | ||||
|             <androidx.appcompat.widget.AppCompatTextView | ||||
|                 android:id="@+id/mods_adapter_item_name" | ||||
|                 style="@style/VCMI.Text.ModName" | ||||
|                 android:ellipsize="end" | ||||
|                 android:lines="1" | ||||
|                 android:text="mod name, v1.0" /> | ||||
|  | ||||
|             <androidx.appcompat.widget.AppCompatTextView | ||||
|                 android:id="@+id/mods_adapter_item_author" | ||||
|                 style="@style/VCMI.Text.ModAuthor" | ||||
|                 android:ellipsize="end" | ||||
|                 android:lines="1" | ||||
|                 android:text="by mod author" /> | ||||
|         </LinearLayout> | ||||
|  | ||||
|         <LinearLayout | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:orientation="vertical"> | ||||
|  | ||||
|             <androidx.appcompat.widget.AppCompatTextView | ||||
|                 android:id="@+id/mods_adapter_item_modtype" | ||||
|                 style="@style/VCMI.Text.ModType" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_gravity="right" | ||||
|                 android:text="tools" /> | ||||
|  | ||||
|             <androidx.appcompat.widget.AppCompatTextView | ||||
|                 android:id="@+id/mods_adapter_item_size" | ||||
|                 style="@style/VCMI.Text.ModSize" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_gravity="right" | ||||
|                 android:text="1000 MB" /> | ||||
|         </LinearLayout> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatImageButton | ||||
|             android:id="@+id/mods_adapter_item_btn_download" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:background="?attr/selectableItemBackgroundBorderless" | ||||
|             android:padding="@dimen/side_margin" | ||||
|             android:src="@android:drawable/stat_sys_download" /> | ||||
|  | ||||
|         <androidx.appcompat.widget.AppCompatImageButton | ||||
|             android:id="@+id/mods_adapter_item_btn_uninstall" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:background="?attr/selectableItemBackgroundBorderless" | ||||
|             android:padding="@dimen/side_margin" | ||||
|             android:src="@android:drawable/ic_menu_delete" /> | ||||
|  | ||||
|         <TextView | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:id="@+id/mods_adapter_item_install_progress" | ||||
|             android:padding="@dimen/side_margin" /> | ||||
|  | ||||
|     </LinearLayout> | ||||
| </LinearLayout> | ||||
| @@ -1,10 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
|  | ||||
|     <item | ||||
|         android:id="@+id/menu_launcher_about" | ||||
|         android:icon="@drawable/ic_info" | ||||
|         android:title="@string/menu_launcher_about" | ||||
|         app:showAsAction="always" /> | ||||
| </menu> | ||||
| @@ -1,9 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
|     <item | ||||
|         android:id="@+id/menu_mods_download_repo" | ||||
|         android:icon="@android:drawable/stat_sys_download" | ||||
|         android:title="@string/menu_mods_download_repo" | ||||
|         app:showAsAction="ifRoom" /> | ||||
| </menu> | ||||
| @@ -1,71 +1,4 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <string name="url_project_page" translatable="false">https://vcmi.eu</string> | ||||
|     <string name="url_project_repo" translatable="false">https://github.com/vcmi/vcmi</string> | ||||
|     <string name="url_launcher_repo" translatable="false">https://github.com/vcmi/vcmi-android</string> | ||||
|     <string name="url_launcher_privacy" translatable="false">https://github.com/vcmi/vcmi/blob/master/docs/players/Privacy_Policy.md</string> | ||||
|  | ||||
|     <string name="app_name">VCMI</string> | ||||
|     <string name="server_name">Server VCMI</string> | ||||
|     <string name="launcher_title">Spouštěč VCMI</string> | ||||
|     <string name="launcher_btn_scale_title">Škálování herního rozlišení</string> | ||||
|     <string name="launcher_btn_scale_subtitle_unknown">Současné: neznámé</string> | ||||
|     <string name="launcher_btn_scale_subtitle">Současné: %1$d%%</string> | ||||
|     <string name="launcher_btn_start_title">Spustit VCMI</string> | ||||
|     <string name="launcher_btn_start_subtitle">Současná verze VCMI: %1$s</string> | ||||
|     <string name="launcher_btn_mods_title">Modifikace</string> | ||||
|     <string name="launcher_btn_mods_subtitle">Nainstalovat nové frakce, přeměty a bonusy</string> | ||||
|     <string name="launcher_btn_language_title">Jazyk</string> | ||||
|     <string name="launcher_btn_language_subtitle_unknown">Současný: neznámý</string> | ||||
|     <string name="launcher_btn_language_subtitle">Současný: %1$s</string> | ||||
|     <string name="launcher_btn_pointermode_title">Změnit režim ukazatele</string> | ||||
|     <string name="launcher_btn_pointermode_subtitle">Současný: %1$s</string> | ||||
|     <string name="launcher_btn_pointermulti_title">Násobitel rychlosti relativního ukazatele</string> | ||||
|     <string name="launcher_btn_pointermulti_subtitle">Současný: %1$s</string> | ||||
|     <string name="launcher_btn_sound_title">Hlasitost zvuků</string> | ||||
|     <string name="launcher_btn_music_title">Hlasitost hudby</string> | ||||
|     <string name="launcher_btn_adventure_ai">AI světa</string> | ||||
|     <string name="launcher_btn_adventure_ai_title">Změnit AI světa</string> | ||||
|     <string name="launcher_btn_import_title">Importovat data VCMI</string> | ||||
|     <string name="launcher_btn_import_description">Zkopírovat soubury VCMI do vestavěného úložiště. Můžete importovat starou složku dat vcmi z vydání 0.99 nebo soubory HOMM3</string> | ||||
|     <string name="launcher_btn_export_title">Exportovat data VCMI</string> | ||||
|     <string name="launcher_btn_export_description">Udělat kopii dat VCMI před odinstalací nebo pro synchronizaci s desktopovou verzí. Též můžete přímo přistoupit k interním datům.</string> | ||||
|     <string name="launcher_progress_copy">Kopírování %1$s</string> | ||||
|     <string name="launcher_version">Současná verze spouštěče: %1$s</string> | ||||
|     <string name="launcher_error_vcmi_data_root_failed">Nelze vytvořit datovou složku VCMI v %1$s.</string> | ||||
|     <string name="launcher_error_h3_data_missing">Nelze najít datovou složku v \'%1$s\'. Vložte do ní své datové soubory HoMM3 nebo použijte tlačítko níže. Možná budete muset také restartovat aplikaci.</string> | ||||
|     <string name="launcher_error_vcmi_data_internal_missing">Nelze najít nebo rozbalit data vcmi ze zdrojů aplikace. Zkuste přeinstalovat aplikaci.</string> | ||||
|     <string name="launcher_error_vcmi_data_internal_update">Nelze aktualizovat data vcmi ze zdrojů aplikace. Zkuste přeinstalovat aplikaci.</string> | ||||
|     <string name="launcher_error_permissions">Tato aplikace potřebuje oprávnění k zápisu pro použití obsahu na externím úložišti</string> | ||||
|     <string name="launcher_error_permission_broken">Nelze správně vyřešit oprávnění</string> | ||||
|     <string name="mods_item_author_template">od %1$s</string> | ||||
|     <string name="misc_try_again">Zkusit znovu</string> | ||||
|     <string name="launcher_section_init">Inicializae hry</string> | ||||
|     <string name="launcher_section_settings">Nastavení</string> | ||||
|     <string name="menu_mods_download_repo">Stáhnout data repozitáře</string> | ||||
|  | ||||
|     <string name="misc_pointermode_normal">Normální</string> | ||||
|     <string name="misc_pointermode_relative">Relativní</string> | ||||
|     <string name="menu_launcher_about">O spouštěči</string> | ||||
|  | ||||
|     <string name="mods_title">Nalezené modifikace</string> | ||||
|     <string name="mods_failed_mod_loading">Nelze načíst modifikaci ve složce \'%1$s\'</string> | ||||
|     <string name="mods_removal_title">Odebírání %1$s</string> | ||||
|     <string name="mods_removal_confirmation">Jste si jisti odebráním %1$s?</string> | ||||
|  | ||||
|     <string name="about_title">O aplikaci</string> | ||||
|     <string name="about_version_app">Verze aplikace: %1$s</string> | ||||
|     <string name="about_version_launcher">Verze spouštěče: %1$s</string> | ||||
|     <string name="about_section_project">Projekt</string> | ||||
|     <string name="about_section_legal">Právní záležitosti</string> | ||||
|     <string name="about_links_main">Hlavní stránka: %1$s</string> | ||||
|     <string name="about_links_repo">Repozitář projektu: %1$s</string> | ||||
|     <string name="about_links_repo_launcher">Repozitář spouštěče: %1$s</string> | ||||
|     <string name="about_btn_authors">Autoři</string> | ||||
|     <string name="about_btn_privacy">Zásady ochrany osobních údajů: %1$s</string> | ||||
|     <string name="about_error_opening_url">Nebylo možné otevřít webovou stránku (nenalezena patřičná aplikace)</string> | ||||
|  | ||||
|     <string name="dialog_authors_vcmi">Autoři VCMI</string> | ||||
|     <string name="dialog_authors_launcher">Autoři spouštěče</string> | ||||
|     <string name="launcher_error_config_saving_failed">Nelze uložit konfigurační soubor VCMI; důvod: %1$s</string> | ||||
| </resources> | ||||
|   | ||||
| @@ -1,62 +1,4 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <string name="app_name">VCMI</string> | ||||
|     <string name="launcher_btn_start_title">VCMI starten</string> | ||||
|     <string name="launcher_title">VCMI-Starter</string> | ||||
|     <string name="launcher_btn_scale_title">Skalierung der Spielauflösung</string> | ||||
|     <string name="launcher_btn_scale_subtitle_unknown">Aktuell: unbekannt</string> | ||||
|     <string name="launcher_btn_scale_subtitle">Aktuell: %1$d%%</string> | ||||
|     <string name="server_name">VCMI-Server</string> | ||||
|     <string name="launcher_btn_start_subtitle">Aktuelle VCMI-Version: %1$s</string> | ||||
|     <string name="launcher_btn_mods_title">Mods</string> | ||||
|     <string name="launcher_btn_mods_subtitle">Neue Burgen, Kreaturen, Objekte und Erweiterungen hinzufügen</string> | ||||
|     <string name="launcher_btn_language_title">Sprache</string> | ||||
|     <string name="launcher_btn_language_subtitle_unknown">Aktuell: unbekannt</string> | ||||
|     <string name="launcher_btn_language_subtitle">Aktuell: %1$s</string> | ||||
|     <string name="launcher_btn_pointermode_title">Zeigermodus ändern</string> | ||||
|     <string name="launcher_btn_pointermode_subtitle">Aktuell: %1$s</string> | ||||
|     <string name="launcher_btn_pointermulti_subtitle">Aktuell: %1$s</string> | ||||
|     <string name="launcher_version">Aktuelle Version des Starters: %1$s</string> | ||||
|     <string name="launcher_error_h3_data_missing">Der Datenordner in \'%1$s\' konnte nicht gefunden werden. Legen Sie Ihre HoMM3-Datendateien dort ab oder verwenden Sie die Schaltfläche unten, um diese zu importieren. Möglicherweise müssen Sie die Anwendung neu starten.</string> | ||||
|     <string name="launcher_btn_pointermulti_title">Multiplikator der relativen Zeigergeschwindigkeit</string> | ||||
|     <string name="launcher_btn_sound_title">Lautstärke der Geräusche</string> | ||||
|     <string name="launcher_btn_music_title">Lautstärke der Musik</string> | ||||
|     <string name="launcher_btn_adventure_ai">Abenteuer KI</string> | ||||
|     <string name="launcher_btn_adventure_ai_title">Abenteuer KI ändern</string> | ||||
|     <string name="launcher_error_vcmi_data_root_failed">VCMI-Datenordner in %1$s konnte nicht erstellt werden.</string> | ||||
|     <string name="launcher_error_vcmi_data_internal_missing">Ressourcendateien konnten nicht extrahiert werden. Versuchen Sie, die Anwendung neu zu installieren.</string> | ||||
|     <string name="launcher_error_vcmi_data_internal_update">Die Ressourcendateien konnten nicht aktualisiert werden. Versuchen Sie, die Anwendung neu zu installieren.</string> | ||||
|     <string name="launcher_error_permissions">Die Anwendung benötigt Rechte, um Inhalte auf externen Speicher zu schreiben.</string> | ||||
|     <string name="launcher_error_permission_broken">Keine Berechtigung erhalten.</string> | ||||
|     <string name="launcher_error_config_saving_failed">Einstellungen konnten nicht gespeichert werden; Grund: %1$s</string> | ||||
|     <string name="mods_item_author_template">Autor %1$s</string> | ||||
|     <string name="misc_try_again">Erneut versuchen</string> | ||||
|     <string name="launcher_section_init">Initialisierung</string> | ||||
|     <string name="launcher_section_settings">Einstellungen</string> | ||||
|     <string name="menu_mods_download_repo">Liste der Mods herunterladen</string> | ||||
|     <string name="misc_pointermode_normal">Normal</string> | ||||
|     <string name="misc_pointermode_relative">Relativ</string> | ||||
|     <string name="menu_launcher_about">Über</string> | ||||
|     <string name="mods_title">Installierte Mods</string> | ||||
|     <string name="mods_failed_mod_loading">Die Mod in dem Ordner \'%1$s\' kann nicht geladen werden.</string> | ||||
|     <string name="mods_removal_title">%1$s löschen</string> | ||||
|     <string name="mods_removal_confirmation">Sind Sie sicher, dass Sie %1$s löschen wollen?</string> | ||||
|     <string name="about_title">Über die Anwendung</string> | ||||
|     <string name="about_version_app">Version der Anwendung: %1$s</string> | ||||
|     <string name="about_version_launcher">Version des Startprogramms: %1$s</string> | ||||
|     <string name="about_section_project">Projekt</string> | ||||
|     <string name="about_section_legal">Rechtliches</string> | ||||
|     <string name="about_links_main">Website: %1$s</string> | ||||
|     <string name="about_links_repo">Projekt-Repository: %1$s</string> | ||||
|     <string name="about_links_repo_launcher">Starter-Repository: %1$s</string> | ||||
|     <string name="about_btn_authors">Autoren</string> | ||||
|     <string name="about_error_opening_url">Die Seite kann nicht geöffnet werden (wahrscheinlich konnte kein Browser gefunden werden)</string> | ||||
|     <string name="dialog_authors_vcmi">VCMI-Autoren</string> | ||||
|     <string name="dialog_authors_launcher">Autoren des Starters</string> | ||||
|     <string name="about_btn_privacy">Datenschutzbestimmungen: %1$s</string> | ||||
|     <string name="launcher_btn_export_title">VCMI-Daten exportieren</string> | ||||
|     <string name="launcher_btn_export_description">Erstellen Sie eine Kopie der internen VCMI-Daten vor der Deinstallation oder zur Synchronisierung der Speicherstände mit der Desktop-Version. Sie können auch direkt auf den internen Speicher zugreifen.</string> | ||||
|     <string name="launcher_btn_import_title">VCMI-Daten importieren</string> | ||||
|     <string name="launcher_btn_import_description">Kopieren Sie VCMI-Dateien in den internen Speicher. Sie können den alten vcmi-data-Ordner von Version 0.99 oder HoMM3-Dateien importieren</string> | ||||
|     <string name="launcher_progress_copy">Kopiere %1$s</string> | ||||
| </resources> | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user