mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 13:50:03 +00:00 
			
		
		
		
	Merge pull request #6004 from SachinVin/android-5-java-dump
Android dump
This commit is contained in:
		
						commit
						4a9995ab9f
					
				
					 372 changed files with 18175 additions and 859 deletions
				
			
		
							
								
								
									
										12
									
								
								.ci/android/build.sh
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										12
									
								
								.ci/android/build.sh
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | #!/bin/bash -ex | ||||||
|  | 
 | ||||||
|  | export NDK_CCACHE=$(which ccache) | ||||||
|  | 
 | ||||||
|  | ccache -s | ||||||
|  | 
 | ||||||
|  | cd src/android | ||||||
|  | chmod +x ./gradlew | ||||||
|  | ./gradlew bundleRelease | ||||||
|  | ./gradlew assembleRelease | ||||||
|  | 
 | ||||||
|  | ccache -s | ||||||
							
								
								
									
										10
									
								
								.ci/android/upload.sh
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										10
									
								
								.ci/android/upload.sh
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | #!/bin/bash -ex | ||||||
|  | 
 | ||||||
|  | . ./.ci/common/pre-upload.sh | ||||||
|  | 
 | ||||||
|  | REV_NAME="citra-${GITDATE}-${GITREV}" | ||||||
|  | 
 | ||||||
|  | cp src/android/app/build/outputs/apk/release/app-release.apk \ | ||||||
|  |   "artifacts/${REV_NAME}.apk" | ||||||
|  | cp src/android/app/build/outputs/bundle/release/app-release.aab \ | ||||||
|  |   "artifacts/${REV_NAME}.aab" | ||||||
							
								
								
									
										32
									
								
								.github/workflows/ci.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								.github/workflows/ci.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -107,6 +107,38 @@ jobs: | ||||||
|         shell: bash |         shell: bash | ||||||
|         env: |         env: | ||||||
|           ENABLE_COMPATIBILITY_REPORTING: "ON" |           ENABLE_COMPATIBILITY_REPORTING: "ON" | ||||||
|  |   android: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |         with: | ||||||
|  |           submodules: recursive | ||||||
|  |       - name: Set up cache | ||||||
|  |         uses: actions/cache@v2 | ||||||
|  |         with: | ||||||
|  |           path: | | ||||||
|  |             ~/.gradle/caches | ||||||
|  |             ~/.gradle/wrapper | ||||||
|  |             ~/.ccache | ||||||
|  |           key: ${{ runner.os }}-android-${{ github.sha }} | ||||||
|  |           restore-keys: | | ||||||
|  |             ${{ runner.os }}-android- | ||||||
|  |       - name: Query tag name | ||||||
|  |         uses: little-core-labs/get-git-tag@v3.0.2 | ||||||
|  |         id: tagName | ||||||
|  |       - name: Deps | ||||||
|  |         run: | | ||||||
|  |           sudo apt-get update | ||||||
|  |           sudo apt-get install ccache -y | ||||||
|  |       - name: Build | ||||||
|  |         run: ./.ci/android/build.sh | ||||||
|  |       - name: Copy artifacts | ||||||
|  |         run: ./.ci/android/upload.sh | ||||||
|  |       - name: Upload | ||||||
|  |         uses: actions/upload-artifact@v2 | ||||||
|  |         with: | ||||||
|  |           name: android | ||||||
|  |           path: artifacts/ | ||||||
|   transifex: |   transifex: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     container: citraemu/build-environments:linux-transifex |     container: citraemu/build-environments:linux-transifex | ||||||
|  |  | ||||||
|  | @ -49,6 +49,14 @@ if(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/hooks/pre-commit) | ||||||
|         DESTINATION ${PROJECT_SOURCE_DIR}/.git/hooks) |         DESTINATION ${PROJECT_SOURCE_DIR}/.git/hooks) | ||||||
| endif() | endif() | ||||||
| 
 | 
 | ||||||
|  | # Use ccache for android if available | ||||||
|  | # ======================================================================= | ||||||
|  | if (NOT $ENV{NDK_CCACHE} EQUAL "") | ||||||
|  |     set(CCACHE_EXE $ENV{NDK_CCACHE}) | ||||||
|  |     set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_EXE}) | ||||||
|  |     set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_EXE}) | ||||||
|  | endif() | ||||||
|  | 
 | ||||||
| # Sanity check : Check that all submodules are present | # Sanity check : Check that all submodules are present | ||||||
| # ======================================================================= | # ======================================================================= | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										150
									
								
								bitrise.yml
									
										
									
									
									
								
							
							
						
						
									
										150
									
								
								bitrise.yml
									
										
									
									
									
								
							|  | @ -1,5 +1,5 @@ | ||||||
| --- | --- | ||||||
| format_version: '6' | format_version: '11' | ||||||
| default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git | ||||||
| project_type: android | project_type: android | ||||||
| trigger_map: | trigger_map: | ||||||
|  | @ -7,118 +7,83 @@ trigger_map: | ||||||
|   workflow: primary |   workflow: primary | ||||||
| - pull_request_source_branch: "*" | - pull_request_source_branch: "*" | ||||||
|   workflow: primary |   workflow: primary | ||||||
|  | - tag: "*" | ||||||
|  |   workflow: deploy | ||||||
| workflows: | workflows: | ||||||
|   deploy: |   deploy: | ||||||
|     description: | |  | ||||||
|       ## How to get a signed APK |  | ||||||
| 
 |  | ||||||
|       This workflow contains the **Sign APK** step. To sign your APK all you have to do is to: |  | ||||||
| 
 |  | ||||||
|       1. Click on **Code Signing** tab |  | ||||||
|       1. Find the **ANDROID KEYSTORE FILE** section |  | ||||||
|       1. Click or drop your file on the upload file field |  | ||||||
|       1. Fill the displayed 3 input fields: |  | ||||||
|        1. **Keystore password** |  | ||||||
|        1. **Keystore alias** |  | ||||||
|        1. **Private key password** |  | ||||||
|       1. Click on **[Save metadata]** button |  | ||||||
| 
 |  | ||||||
|       That's it! From now on, **Sign APK** step will receive your uploaded files. |  | ||||||
| 
 |  | ||||||
|       ## To run this workflow |  | ||||||
| 
 |  | ||||||
|       If you want to run this workflow manually: |  | ||||||
| 
 |  | ||||||
|       1. Open the app's build list page |  | ||||||
|       2. Click on **[Start/Schedule a Build]** button |  | ||||||
|       3. Select **deploy** in **Workflow** dropdown input |  | ||||||
|       4. Click **[Start Build]** button |  | ||||||
| 
 |  | ||||||
|       Or if you need this workflow to be started by a GIT event: |  | ||||||
| 
 |  | ||||||
|       1. Click on **Triggers** tab |  | ||||||
|       2. Setup your desired event (push/tag/pull) and select **deploy** workflow |  | ||||||
|       3. Click on **[Done]** and then **[Save]** buttons |  | ||||||
| 
 |  | ||||||
|       The next change in your repository that matches any of your trigger map event will start **deploy** workflow. |  | ||||||
|     steps: |     steps: | ||||||
|     - cache-pull@2.4.0: {} |     - activate-ssh-key@4: {} | ||||||
|     - script@1.1.6: |     - git-clone@6: {} | ||||||
|  |     - cache-pull@2: {} | ||||||
|  |     - script@1: | ||||||
|         title: Install newer cmake |         title: Install newer cmake | ||||||
|         inputs: |         inputs: | ||||||
|             - content: |- |         - content: |- | ||||||
|                 #!/bin/bash |             #!/bin/bash | ||||||
|                 set -ex |             set -ex | ||||||
|                 sudo apt remove cmake -y |             sdkmanager --install "cmake;3.18.1" | ||||||
|                 sudo apt purge --auto-remove cmake -y |     - install-missing-android-tools@2.3: | ||||||
|                 sudo apt install ninja-build -y |  | ||||||
|                 version=3.19 |  | ||||||
|                 build=2 |  | ||||||
|                 mkdir ~/temp |  | ||||||
|                 cd ~/temp |  | ||||||
|                 wget https://cmake.org/files/v$version/cmake-$version.$build-Linux-x86_64.sh |  | ||||||
|                 sudo mkdir /opt/cmake |  | ||||||
|                 sudo sh cmake-$version.$build-Linux-x86_64.sh --prefix=/opt/cmake --skip-license --exclude-subdir |  | ||||||
|                 envman add --key PATH --value "/opt/cmake/bin:$PATH" |  | ||||||
|     - install-missing-android-tools@2.3.8: |  | ||||||
|         inputs: |         inputs: | ||||||
|         - gradlew_path: "$PROJECT_LOCATION/gradlew" |         - gradlew_path: "$PROJECT_LOCATION/gradlew" | ||||||
|     - change-android-versioncode-and-versionname@1.1.1: |     - android-lint@0: | ||||||
|         inputs: |  | ||||||
|         - build_gradle_path: "$PROJECT_LOCATION/$MODULE/build.gradle" |  | ||||||
|     - android-lint@0.9.8: |  | ||||||
|         inputs: |  | ||||||
|         - project_location: "$PROJECT_LOCATION" |  | ||||||
|         - module: "$MODULE" |  | ||||||
|         - variant: "$TEST_VARIANT" |  | ||||||
|     - android-unit-test@0.9.3: |  | ||||||
|         inputs: |  | ||||||
|         - project_location: "$PROJECT_LOCATION" |  | ||||||
|         - module: "$MODULE" |  | ||||||
|         - variant: "$TEST_VARIANT" |  | ||||||
|     - android-build@0.10.3: |  | ||||||
|         inputs: |         inputs: | ||||||
|         - project_location: "$PROJECT_LOCATION" |         - project_location: "$PROJECT_LOCATION" | ||||||
|         - module: "$MODULE" |         - module: "$MODULE" | ||||||
|         - variant: "$BUILD_VARIANT" |         - variant: "$BUILD_VARIANT" | ||||||
|     - sign-apk@1.2.3: |     - android-build@0: | ||||||
|  |         inputs: | ||||||
|  |         - variant: "$BUILD_VARIANT" | ||||||
|  |         - project_location: "$PROJECT_LOCATION" | ||||||
|  |         - build_type: aab | ||||||
|  |     - sign-apk@1: | ||||||
|         run_if: '{{getenv "BITRISEIO_ANDROID_KEYSTORE_URL" | ne ""}}' |         run_if: '{{getenv "BITRISEIO_ANDROID_KEYSTORE_URL" | ne ""}}' | ||||||
|     - deploy-to-bitrise-io@1.11.1: {} |     - bitrise-step-export-universal-apk@0: | ||||||
|     - cache-push@2.4.1: {} |         run_if: '{{getenv "BITRISEIO_ANDROID_KEYSTORE_URL" | ne ""}}' | ||||||
|  |     - generate-changelog@0: {} | ||||||
|  |     - github-release@0: | ||||||
|  |         run_if: '{{getenv "GITHUB_API_TOKEN" | ne ""}}' | ||||||
|  |         inputs: | ||||||
|  |         - api_token: "$GITHUB_API_TOKEN" | ||||||
|  |         - name: "$BITRISE_GIT_TAG" | ||||||
|  |         - body: "$BITRISE_CHANGELOG" | ||||||
|  |         - files_to_upload: |- | ||||||
|  |             $BITRISE_AAB_PATH|citra-$BITRISE_GIT_TAG.aab | ||||||
|  |             $BITRISE_APK_PATH|citra-$BITRISE_GIT_TAG.apk | ||||||
|  |         - username: "$BITRISEIO_GIT_REPOSITORY_OWNER" | ||||||
|  |     - deploy-to-bitrise-io@1.3: | ||||||
|  |         run_if: '{{getenv "BITRISEIO_ANDROID_KEYSTORE_URL" | ne ""}}' | ||||||
|  |     - cache-push@2: {} | ||||||
|  |     - deploy-to-bitrise-io@2: {} | ||||||
|   primary: |   primary: | ||||||
|     steps: |     steps: | ||||||
|     - cache-pull@2.4.0: {} |     - activate-ssh-key@4: {} | ||||||
|     - script@1.1.6: |     - git-clone@6: {} | ||||||
|         title: Install newer cmake |     - cache-pull@2: {} | ||||||
|  |     - script@1: | ||||||
|  |         title: Deps | ||||||
|         inputs: |         inputs: | ||||||
|             - content: |- |         - content: |- | ||||||
|                 #!/bin/bash |             #!/bin/bash | ||||||
|                 set -ex |             set -ex | ||||||
|                 sudo apt remove cmake -y |             sdkmanager --install "cmake;3.18.1" | ||||||
|                 sudo apt purge --auto-remove cmake -y |     - install-missing-android-tools@3: | ||||||
|                 sudo apt install ninja-build -y |  | ||||||
|                 version=3.19 |  | ||||||
|                 build=2 |  | ||||||
|                 mkdir ~/temp |  | ||||||
|                 cd ~/temp |  | ||||||
|                 wget https://cmake.org/files/v$version/cmake-$version.$build-Linux-x86_64.sh |  | ||||||
|                 sudo mkdir /opt/cmake |  | ||||||
|                 sudo sh cmake-$version.$build-Linux-x86_64.sh --prefix=/opt/cmake --skip-license --exclude-subdir |  | ||||||
|                 envman add --key PATH --value "/opt/cmake/bin:$PATH" |  | ||||||
|     - install-missing-android-tools@2.3.8: |  | ||||||
|         inputs: |         inputs: | ||||||
|             - gradlew_path: "$PROJECT_LOCATION/gradlew" |         - gradlew_path: "$PROJECT_LOCATION/gradlew" | ||||||
|     - android-lint@0.9.8: |     - android-lint@0: | ||||||
|         inputs: |         inputs: | ||||||
|         - project_location: "$PROJECT_LOCATION" |         - project_location: "$PROJECT_LOCATION" | ||||||
|         - module: "$MODULE" |         - module: "$MODULE" | ||||||
|         - variant: "$TEST_VARIANT" |         - variant: "$BUILD_VARIANT" | ||||||
|     - android-build@0.10.3: |     - android-build@1: | ||||||
|         inputs: |         inputs: | ||||||
|         - variant: Debug |         - variant: "$BUILD_VARIANT" | ||||||
|         - project_location: "$PROJECT_LOCATION" |         - project_location: "$PROJECT_LOCATION" | ||||||
|     - deploy-to-bitrise-io@1.11.1: {} |         - build_type: apk | ||||||
|     - cache-push@2.4.1: {} |     - cache-push@2: {} | ||||||
|  |     - deploy-to-bitrise-io@2: {} | ||||||
|  | meta: | ||||||
|  |   bitrise.io: | ||||||
|  |     stack: linux-docker-android-20.04 | ||||||
| app: | app: | ||||||
|   envs: |   envs: | ||||||
|   - opts: |   - opts: | ||||||
|  | @ -132,4 +97,3 @@ app: | ||||||
|     BUILD_VARIANT: Release |     BUILD_VARIANT: Release | ||||||
|   - opts: |   - opts: | ||||||
|       is_expand: false |       is_expand: false | ||||||
|     TEST_VARIANT: Debug |  | ||||||
|  |  | ||||||
|  | @ -117,8 +117,10 @@ endif() | ||||||
| if (ENABLE_QT) | if (ENABLE_QT) | ||||||
|     add_subdirectory(citra_qt) |     add_subdirectory(citra_qt) | ||||||
| endif() | endif() | ||||||
|  | 
 | ||||||
| if (ANDROID) | if (ANDROID) | ||||||
|     add_subdirectory(android/app/src/main/cpp) |     add_subdirectory(android/app/src/main/jni) | ||||||
|  |     target_include_directories(citra-android PRIVATE android/app/src/main) | ||||||
| else() | else() | ||||||
|     add_subdirectory(dedicated_room) |     add_subdirectory(dedicated_room) | ||||||
| endif() | endif() | ||||||
|  |  | ||||||
							
								
								
									
										50
									
								
								src/android/.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										50
									
								
								src/android/.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,12 +1,46 @@ | ||||||
|  | # Built application files | ||||||
|  | *.apk | ||||||
|  | *.ap_ | ||||||
|  | 
 | ||||||
|  | # Files for the ART/Dalvik VM | ||||||
|  | *.dex | ||||||
|  | 
 | ||||||
|  | # Java class files | ||||||
|  | *.class | ||||||
|  | 
 | ||||||
|  | # Generated files | ||||||
|  | bin/ | ||||||
|  | gen/ | ||||||
|  | out/ | ||||||
|  | 
 | ||||||
|  | # Gradle files | ||||||
|  | .gradle/ | ||||||
|  | build/ | ||||||
|  | 
 | ||||||
|  | # Local configuration file (sdk path, etc) | ||||||
|  | local.properties | ||||||
|  | 
 | ||||||
|  | # Proguard folder generated by Eclipse | ||||||
|  | proguard/ | ||||||
|  | 
 | ||||||
|  | # Log Files | ||||||
|  | *.log | ||||||
|  | 
 | ||||||
|  | # Android Studio Navigation editor temp files | ||||||
|  | .navigation/ | ||||||
|  | 
 | ||||||
|  | # Android Studio captures folder | ||||||
|  | captures/ | ||||||
|  | 
 | ||||||
|  | # IntelliJ | ||||||
| *.iml | *.iml | ||||||
| .gradle | .idea/ | ||||||
| /local.properties | 
 | ||||||
| /.idea/libraries | # Keystore files | ||||||
| /.idea/modules.xml | # Uncomment the following line if you do not want to check your keystore files in. | ||||||
| /.idea/workspace.xml | #*.jks | ||||||
| .DS_Store | 
 | ||||||
| /build | # External native build folder generated in Android Studio 2.2 and later | ||||||
| /captures |  | ||||||
| .externalNativeBuild | .externalNativeBuild | ||||||
| 
 | 
 | ||||||
| # CXX compile cache | # CXX compile cache | ||||||
|  |  | ||||||
|  | @ -1,8 +1,17 @@ | ||||||
| apply plugin: 'com.android.application' | apply plugin: 'com.android.application' | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Use the number of seconds/10 since Jan 1 2016 as the versionCode. | ||||||
|  |  * This lets us upload a new build at most every 10 seconds for the | ||||||
|  |  * next 680 years. | ||||||
|  |  */ | ||||||
|  | def autoVersion = (int) (((new Date().getTime() / 1000) - 1451606400) / 10) | ||||||
|  | def buildType | ||||||
|  | def abiFilter = "arm64-v8a" //, "x86" | ||||||
|  | 
 | ||||||
| android { | android { | ||||||
|     compileSdkVersion 26 |     compileSdkVersion 29 | ||||||
|     buildToolsVersion '28.0.3' |     ndkVersion "23.1.7779620" | ||||||
| 
 | 
 | ||||||
|     compileOptions { |     compileOptions { | ||||||
|         sourceCompatibility JavaVersion.VERSION_1_8 |         sourceCompatibility JavaVersion.VERSION_1_8 | ||||||
|  | @ -13,34 +22,54 @@ android { | ||||||
|         // This is important as it will run lint but not abort on error |         // This is important as it will run lint but not abort on error | ||||||
|         // Lint has some overly obnoxious "errors" that should really be warnings |         // Lint has some overly obnoxious "errors" that should really be warnings | ||||||
|         abortOnError false |         abortOnError false | ||||||
|  | 
 | ||||||
|  |         //Uncomment disable lines for test builds... | ||||||
|  |         //disable 'MissingTranslation'bin | ||||||
|  |         //disable 'ExtraTranslation' | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     defaultConfig { |     defaultConfig { | ||||||
|         applicationId "org.citra_emu" |         // TODO If this is ever modified, change application_id in strings.xml | ||||||
|         minSdkVersion 21 |         applicationId "org.citra.citra_emu" | ||||||
|         targetSdkVersion 26 |         minSdkVersion 26 | ||||||
| 
 |         targetSdkVersion 29 | ||||||
|         versionCode(getBuildVersionCode()) |         versionCode autoVersion | ||||||
| 
 |         versionName getVersion() | ||||||
|         versionName "${getVersion()}" |         ndk.abiFilters abiFilter | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     signingConfigs { |     signingConfigs { | ||||||
|         release { |         //release { | ||||||
|             if (project.hasProperty('keystore')) { |         //    storeFile file('') | ||||||
|                 storeFile file(project.property('keystore')) |         //    storePassword System.getenv('ANDROID_KEYPASS') | ||||||
|                 storePassword project.property('storepass') |         //    keyAlias = 'key0' | ||||||
|                 keyAlias project.property('keyalias') |         //    keyPassword System.getenv('ANDROID_KEYPASS') | ||||||
|                 keyPassword project.property('keypass') |         //} | ||||||
|             } |     } | ||||||
|         } | 
 | ||||||
|  |     applicationVariants.all { variant -> | ||||||
|  |         buildType = variant.buildType.name // sets the current build type | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Define build types, which are orthogonal to product flavors. |     // Define build types, which are orthogonal to product flavors. | ||||||
|     buildTypes { |     buildTypes { | ||||||
|  | 
 | ||||||
|         // Signed by release key, allowing for upload to Play Store. |         // Signed by release key, allowing for upload to Play Store. | ||||||
|         release { |         release { | ||||||
|             signingConfig signingConfigs.release |             signingConfig signingConfigs.debug | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // builds a release build that doesn't need signing | ||||||
|  |         // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build. | ||||||
|  |         relWithDebInfo { | ||||||
|  |             initWith release | ||||||
|  |             applicationIdSuffix ".debug" | ||||||
|  |             versionNameSuffix '-debug' | ||||||
|  |             signingConfig signingConfigs.debug | ||||||
|  |             minifyEnabled false | ||||||
|  |             testCoverageEnabled false | ||||||
|  |             debuggable true | ||||||
|  |             jniDebuggable true | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Signed by debug key disallowing distribution on Play Store. |         // Signed by debug key disallowing distribution on Play Store. | ||||||
|  | @ -49,13 +78,14 @@ android { | ||||||
|             // TODO If this is ever modified, change application_id in debug/strings.xml |             // TODO If this is ever modified, change application_id in debug/strings.xml | ||||||
|             applicationIdSuffix ".debug" |             applicationIdSuffix ".debug" | ||||||
|             versionNameSuffix '-debug' |             versionNameSuffix '-debug' | ||||||
|  |             debuggable true | ||||||
|             jniDebuggable true |             jniDebuggable true | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     externalNativeBuild { |     externalNativeBuild { | ||||||
|         cmake { |         cmake { | ||||||
|             version getCmakeVersion() |             version "3.18.1" | ||||||
|             path "../../../CMakeLists.txt" |             path "../../../CMakeLists.txt" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -65,76 +95,46 @@ android { | ||||||
|             cmake { |             cmake { | ||||||
|                 arguments "-DENABLE_QT=0", // Don't use QT |                 arguments "-DENABLE_QT=0", // Don't use QT | ||||||
|                         "-DENABLE_SDL2=0", // Don't use SDL |                         "-DENABLE_SDL2=0", // Don't use SDL | ||||||
|                         "-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work |                         "-DENABLE_WEB_SERVICE=0", // Don't use telemetry | ||||||
|                         "-DENABLE_CUBEB=0", |                         "-DANDROID_ARM_NEON=true" // cryptopp requires Neon to work | ||||||
|                         "-DANDROID_STL=c++_shared" |  | ||||||
| 
 | 
 | ||||||
|                 abiFilters "arm64-v8a" |                 abiFilters abiFilter | ||||||
| 
 |  | ||||||
|                 targets "citra-android" |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ext { |  | ||||||
|     androidSupportVersion = '26.1.0' |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| dependencies { | dependencies { | ||||||
|     implementation "com.android.support:support-v13:$androidSupportVersion" |     implementation 'androidx.appcompat:appcompat:1.1.0' | ||||||
|     implementation "com.android.support:cardview-v7:$androidSupportVersion" |     implementation 'androidx.exifinterface:exifinterface:1.2.0' | ||||||
|     implementation "com.android.support:recyclerview-v7:$androidSupportVersion" |     implementation 'androidx.cardview:cardview:1.0.0' | ||||||
|     implementation "com.android.support:design:$androidSupportVersion" |     implementation 'androidx.recyclerview:recyclerview:1.1.0' | ||||||
|  |     implementation 'com.google.android.material:material:1.1.0' | ||||||
| 
 | 
 | ||||||
|     // Android TV UI libraries. |     // For loading huge screenshots from the disk. | ||||||
|     implementation "com.android.support:leanback-v17:$androidSupportVersion" |     implementation 'com.squareup.picasso:picasso:2.71828' | ||||||
| 
 | 
 | ||||||
|     implementation 'com.android.support.constraint:constraint-layout:1.1.0' |     // Allows FRP-style asynchronous operations in Android. | ||||||
|  |     implementation 'io.reactivex:rxandroid:1.2.1' | ||||||
|  |     implementation 'com.nononsenseapps:filepicker:4.2.1' | ||||||
|  |     implementation 'org.ini4j:ini4j:0.5.4' | ||||||
|  |     implementation 'androidx.constraintlayout:constraintlayout:1.1.3' | ||||||
|  |     implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' | ||||||
|  |     implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' | ||||||
| 
 | 
 | ||||||
|     testImplementation "com.android.support.test:runner:1.0.2" |     implementation 'com.android.billingclient:billing:2.0.3' | ||||||
|     androidTestImplementation "com.android.support.test:runner:1.0.1" |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| def getVersion() { | def getVersion() { | ||||||
|     def versionNumber = '0.0' |     def versionName = '0.0' | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|         versionNumber = 'git describe --always --long'.execute([], project.rootDir).text |         versionName = 'git describe --always --long'.execute([], project.rootDir).text | ||||||
|                 .trim() |                 .trim() | ||||||
|                 .replaceAll(/(-0)?-[^-]+$/, "") |                 .replaceAll(/(-0)?-[^-]+$/, "") | ||||||
|     } catch (Exception e) { |     } catch (Exception) { | ||||||
|         logger.error('Cannot find git, defaulting to dummy version number') |         logger.error('Cannot find git, defaulting to dummy version number') | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return versionNumber |     return versionName | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def getBuildVersionCode() { |  | ||||||
|     try { |  | ||||||
|         def versionNumber = 'git rev-list --first-parent --count HEAD'.execute([], project.rootDir).text |  | ||||||
|                 .trim() |  | ||||||
|         return Integer.valueOf(versionNumber) |  | ||||||
|     } catch (Exception e) { |  | ||||||
|         logger.error('Cannot find git, defaulting to dummy version number') |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return 0 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| def getCmakeVersion() { |  | ||||||
|     try { |  | ||||||
|         // Tokenized form of the output will be - ["cmake", "version", "M.m.p-rcx"], the version number |  | ||||||
|         // will be at index 2 |  | ||||||
|         def version_string = 'cmake -version'.execute([], project.rootDir).text |  | ||||||
|                 .trim().tokenize()[2] |  | ||||||
| 
 |  | ||||||
|         return version_string |  | ||||||
|     } |  | ||||||
|     catch(Exception e) { |  | ||||||
|         logger.error('Cannot find Cmake, using default Cmake') |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return null |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| package org.citra_emu.citra; | package org.citra.citra_emu; | ||||||
| 
 | 
 | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.support.test.InstrumentationRegistry; | import android.support.test.InstrumentationRegistry; | ||||||
|  | @ -21,6 +21,6 @@ public class ExampleInstrumentedTest { | ||||||
|         // Context of the app under test. |         // Context of the app under test. | ||||||
|         Context appContext = InstrumentationRegistry.getTargetContext(); |         Context appContext = InstrumentationRegistry.getTargetContext(); | ||||||
| 
 | 
 | ||||||
|         assertEquals("org.citra_emu.citra_android", appContext.getPackageName()); |         assertEquals("org.citra.citra_emu", appContext.getPackageName()); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -1,39 +1,91 @@ | ||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     package="org.citra_emu.citra"> |     package="org.citra.citra_emu"> | ||||||
|     <uses-feature |     <uses-feature | ||||||
|         android:name="android.hardware.touchscreen" |         android:name="android.hardware.touchscreen" | ||||||
|         android:required="false"/> |         android:required="false"/> | ||||||
| 
 |  | ||||||
|     <uses-feature |     <uses-feature | ||||||
|         android:name="android.hardware.gamepad" |         android:name="android.hardware.gamepad" | ||||||
|         android:required="false"/> |         android:required="false"/> | ||||||
| 
 | 
 | ||||||
|     <uses-feature android:glEsVersion="0x00030001" /> |     <uses-feature android:glEsVersion="0x00030002" android:required="true" /> | ||||||
|  | 
 | ||||||
|  |     <uses-feature android:name="android.hardware.opengles.aep" android:required="true" /> | ||||||
|  |     <uses-feature | ||||||
|  |         android:name="android.hardware.camera.any" | ||||||
|  |         android:required="false" /> | ||||||
| 
 | 
 | ||||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> |     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||||
|  |     <uses-permission android:name="android.permission.CAMERA" /> | ||||||
|  |     <uses-permission android:name="android.permission.RECORD_AUDIO" /> | ||||||
|  |     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     <application |     <application | ||||||
|         android:name="org.citra_emu.citra.CitraApplication" |         android:name="org.citra.citra_emu.CitraApplication" | ||||||
|         android:label="Citra" |         android:label="@string/app_name" | ||||||
|         android:icon="@mipmap/ic_citra" |         android:icon="@mipmap/ic_launcher" | ||||||
|         android:allowBackup="true" |         android:allowBackup="false" | ||||||
|         android:supportsRtl="true" |         android:supportsRtl="true" | ||||||
|         android:isGame="true" |         android:isGame="true" | ||||||
|         android:banner="@mipmap/ic_citra"> |         android:banner="@mipmap/ic_launcher" | ||||||
|  |         android:requestLegacyExternalStorage="true"> | ||||||
| 
 | 
 | ||||||
|         <activity |         <activity | ||||||
|             android:name=".ui.main.MainActivity" |             android:name="org.citra.citra_emu.ui.main.MainActivity" | ||||||
|             android:theme="@style/CitraBase"> |             android:theme="@style/CitraBase" | ||||||
|  |             android:resizeableActivity="false"> | ||||||
| 
 | 
 | ||||||
|             <!-- This intentfilter marks this Activity as the one that gets launched from Home screen. --> |             <!-- This intentfilter marks this Activity as the one that gets launched from Home screen. --> | ||||||
|             <intent-filter> |             <intent-filter> | ||||||
|                 <action android:name="android.intent.action.MAIN"/> |                 <action android:name="android.intent.action.MAIN"/> | ||||||
|                 <action android:name="android.intent.action.VIEW"/> |  | ||||||
| 
 | 
 | ||||||
|                 <category android:name="android.intent.category.LAUNCHER"/> |                 <category android:name="android.intent.category.LAUNCHER"/> | ||||||
|             </intent-filter> |             </intent-filter> | ||||||
|         </activity> |         </activity> | ||||||
|  | 
 | ||||||
|  |         <activity | ||||||
|  |             android:name="org.citra.citra_emu.features.settings.ui.SettingsActivity" | ||||||
|  |             android:configChanges="orientation|screenSize|uiMode" | ||||||
|  |             android:theme="@style/CitraSettingsBase" | ||||||
|  |             android:label="@string/preferences_settings"/> | ||||||
|  | 
 | ||||||
|  |         <activity | ||||||
|  |             android:name="org.citra.citra_emu.activities.EmulationActivity" | ||||||
|  |             android:resizeableActivity="false" | ||||||
|  |             android:theme="@style/CitraEmulationBase" | ||||||
|  |             android:launchMode="singleTop"/> | ||||||
|  | 
 | ||||||
|  |         <service android:name="org.citra.citra_emu.utils.ForegroundService"/> | ||||||
|  | 
 | ||||||
|  |         <activity | ||||||
|  |             android:name="org.citra.citra_emu.activities.CustomFilePickerActivity" | ||||||
|  |             android:label="@string/app_name" | ||||||
|  |             android:theme="@style/FilePickerTheme"> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="android.intent.action.GET_CONTENT" /> | ||||||
|  |                 <category android:name="android.intent.category.DEFAULT" /> | ||||||
|  |             </intent-filter> | ||||||
|  |         </activity> | ||||||
|  | 
 | ||||||
|  |         <service android:name="org.citra.citra_emu.utils.DirectoryInitialization"/> | ||||||
|  | 
 | ||||||
|  |         <provider | ||||||
|  |             android:name="org.citra.citra_emu.model.GameProvider" | ||||||
|  |             android:authorities="${applicationId}.provider" | ||||||
|  |             android:enabled="true" | ||||||
|  |             android:exported="false"> | ||||||
|  |         </provider> | ||||||
|  | 
 | ||||||
|  |         <provider | ||||||
|  |             android:name="androidx.core.content.FileProvider" | ||||||
|  |             android:authorities="${applicationId}.filesprovider" | ||||||
|  |             android:exported="false" | ||||||
|  |             android:grantUriPermissions="true"> | ||||||
|  |             <meta-data | ||||||
|  |                 android:name="android.support.FILE_PROVIDER_PATHS" | ||||||
|  |                 android:resource="@xml/nnf_provider_paths" /> | ||||||
|  |         </provider> | ||||||
|     </application> |     </application> | ||||||
| 
 | 
 | ||||||
| </manifest> | </manifest> | ||||||
|  |  | ||||||
|  | @ -1,16 +0,0 @@ | ||||||
| cmake_minimum_required(VERSION 3.8) |  | ||||||
| 
 |  | ||||||
| add_library(citra-android SHARED |  | ||||||
|             logging/log.cpp |  | ||||||
|             logging/logcat_backend.cpp |  | ||||||
|             logging/logcat_backend.h |  | ||||||
|             native_interface.cpp |  | ||||||
|             native_interface.h |  | ||||||
|             ui/main/main_activity.cpp |  | ||||||
|             ) |  | ||||||
| 
 |  | ||||||
| # find Android's log library |  | ||||||
| find_library(log-lib log) |  | ||||||
| 
 |  | ||||||
| target_link_libraries(citra-android ${log-lib} core common inih) |  | ||||||
| target_include_directories(citra-android PRIVATE "../../../../../" "./") |  | ||||||
|  | @ -1,15 +0,0 @@ | ||||||
| #include "common/logging/log.h" |  | ||||||
| #include "native_interface.h" |  | ||||||
| 
 |  | ||||||
| namespace Log { |  | ||||||
| extern "C" { |  | ||||||
| JNICALL void Java_org_citra_1emu_citra_LOG_logEntry(JNIEnv* env, jclass type, jint level, |  | ||||||
|                                                     jstring file_name, jint line_number, |  | ||||||
|                                                     jstring function, jstring msg) { |  | ||||||
|     using CitraJNI::GetJString; |  | ||||||
|     FmtLogMessage(Class::Frontend, static_cast<Level>(level), GetJString(env, file_name).data(), |  | ||||||
|                   static_cast<unsigned int>(line_number), GetJString(env, function).data(), |  | ||||||
|                   GetJString(env, msg).data()); |  | ||||||
| } |  | ||||||
| } |  | ||||||
| } // namespace Log
 |  | ||||||
|  | @ -1,38 +0,0 @@ | ||||||
| // Copyright 2019 Citra Emulator Project
 |  | ||||||
| // Licensed under GPLv2 or any later version
 |  | ||||||
| // Refer to the license.txt file included.
 |  | ||||||
| 
 |  | ||||||
| #include <android/log.h> |  | ||||||
| #include "common/assert.h" |  | ||||||
| #include "common/logging/text_formatter.h" |  | ||||||
| #include "logcat_backend.h" |  | ||||||
| 
 |  | ||||||
| namespace Log { |  | ||||||
| void LogcatBackend::Write(const Entry& entry) { |  | ||||||
|     android_LogPriority priority; |  | ||||||
|     switch (entry.log_level) { |  | ||||||
|     case Level::Trace: |  | ||||||
|         priority = ANDROID_LOG_VERBOSE; |  | ||||||
|         break; |  | ||||||
|     case Level::Debug: |  | ||||||
|         priority = ANDROID_LOG_DEBUG; |  | ||||||
|         break; |  | ||||||
|     case Level::Info: |  | ||||||
|         priority = ANDROID_LOG_INFO; |  | ||||||
|         break; |  | ||||||
|     case Level::Warning: |  | ||||||
|         priority = ANDROID_LOG_WARN; |  | ||||||
|         break; |  | ||||||
|     case Level::Error: |  | ||||||
|         priority = ANDROID_LOG_ERROR; |  | ||||||
|         break; |  | ||||||
|     case Level::Critical: |  | ||||||
|         priority = ANDROID_LOG_FATAL; |  | ||||||
|         break; |  | ||||||
|     case Level::Count: |  | ||||||
|         UNREACHABLE(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     __android_log_print(priority, "citra", "%s\n", FormatLogMessage(entry).c_str()); |  | ||||||
| } |  | ||||||
| } // namespace Log
 |  | ||||||
|  | @ -1,22 +0,0 @@ | ||||||
| // Copyright 2019 Citra Emulator Project
 |  | ||||||
| // Licensed under GPLv2 or any later version
 |  | ||||||
| // Refer to the license.txt file included.
 |  | ||||||
| 
 |  | ||||||
| #pragma once |  | ||||||
| 
 |  | ||||||
| #include "common/logging/backend.h" |  | ||||||
| 
 |  | ||||||
| namespace Log { |  | ||||||
| class LogcatBackend : public Backend { |  | ||||||
| public: |  | ||||||
|     static const char* Name() { |  | ||||||
|         return "Logcat"; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const char* GetName() const override { |  | ||||||
|         return Name(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     void Write(const Entry& entry) override; |  | ||||||
| }; |  | ||||||
| } // namespace Log
 |  | ||||||
|  | @ -1,22 +0,0 @@ | ||||||
| // Copyright 2019 Citra Emulator Project
 |  | ||||||
| // Licensed under GPLv2 or any later version
 |  | ||||||
| // Refer to the license.txt file included.
 |  | ||||||
| 
 |  | ||||||
| #include "native_interface.h" |  | ||||||
| 
 |  | ||||||
| namespace CitraJNI { |  | ||||||
| jint JNI_OnLoad(JavaVM* vm, void* reserved) { |  | ||||||
|     return JNI_VERSION_1_6; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| std::string GetJString(JNIEnv* env, jstring jstr) { |  | ||||||
|     std::string result = ""; |  | ||||||
|     if (!jstr) |  | ||||||
|         return result; |  | ||||||
| 
 |  | ||||||
|     const char* s = env->GetStringUTFChars(jstr, nullptr); |  | ||||||
|     result = s; |  | ||||||
|     env->ReleaseStringUTFChars(jstr, s); |  | ||||||
|     return result; |  | ||||||
| } |  | ||||||
| } // namespace CitraJNI
 |  | ||||||
|  | @ -1,16 +0,0 @@ | ||||||
| // Copyright 2019 Citra Emulator Project
 |  | ||||||
| // Licensed under GPLv2 or any later version
 |  | ||||||
| // Refer to the license.txt file included.
 |  | ||||||
| 
 |  | ||||||
| #pragma once |  | ||||||
| 
 |  | ||||||
| #include <string> |  | ||||||
| #include <jni.h> |  | ||||||
| 
 |  | ||||||
| namespace CitraJNI { |  | ||||||
| extern "C" { |  | ||||||
| jint JNI_OnLoad(JavaVM* vm, void* reserved); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| std::string GetJString(JNIEnv* env, jstring jstr); |  | ||||||
| } // namespace CitraJNI
 |  | ||||||
|  | @ -1,31 +0,0 @@ | ||||||
| // Copyright 2019 Citra Emulator Project
 |  | ||||||
| // Licensed under GPLv2 or any later version
 |  | ||||||
| // Refer to the license.txt file included.
 |  | ||||||
| 
 |  | ||||||
| #include "common/common_paths.h" |  | ||||||
| #include "common/file_util.h" |  | ||||||
| #include "common/logging/filter.h" |  | ||||||
| #include "common/logging/log.h" |  | ||||||
| #include "core/settings.h" |  | ||||||
| #include "logging/logcat_backend.h" |  | ||||||
| #include "native_interface.h" |  | ||||||
| 
 |  | ||||||
| namespace MainActivity { |  | ||||||
| extern "C" { |  | ||||||
| JNICALL void Java_org_citra_1emu_citra_ui_main_MainActivity_initUserPath(JNIEnv* env, jclass type, |  | ||||||
|                                                                          jstring path) { |  | ||||||
|     FileUtil::SetUserPath(CitraJNI::GetJString(env, path) + '/'); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| JNICALL void Java_org_citra_1emu_citra_ui_main_MainActivity_initLogging(JNIEnv* env, jclass type) { |  | ||||||
|     Log::Filter log_filter(Log::Level::Debug); |  | ||||||
|     log_filter.ParseFilterString(Settings::values.log_filter); |  | ||||||
|     Log::SetGlobalFilter(log_filter); |  | ||||||
| 
 |  | ||||||
|     const std::string& log_dir = FileUtil::GetUserPath(FileUtil::UserPath::LogDir); |  | ||||||
|     FileUtil::CreateFullPath(log_dir); |  | ||||||
|     Log::AddBackend(std::make_unique<Log::FileBackend>(log_dir + LOG_FILE)); |  | ||||||
|     Log::AddBackend(std::make_unique<Log::LogcatBackend>()); |  | ||||||
| } |  | ||||||
| }; |  | ||||||
| }; // namespace MainActivity
 |  | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 43 KiB | 
|  | @ -0,0 +1,56 @@ | ||||||
|  | // Copyright 2019 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu; | ||||||
|  | 
 | ||||||
|  | import android.app.Application; | ||||||
|  | import android.app.NotificationChannel; | ||||||
|  | import android.app.NotificationManager; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.os.Build; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.model.GameDatabase; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryInitialization; | ||||||
|  | import org.citra.citra_emu.utils.PermissionsHandler; | ||||||
|  | 
 | ||||||
|  | public class CitraApplication extends Application { | ||||||
|  |     public static GameDatabase databaseHelper; | ||||||
|  |     private static CitraApplication application; | ||||||
|  | 
 | ||||||
|  |     private void createNotificationChannel() { | ||||||
|  |         // Create the NotificationChannel, but only on API 26+ because | ||||||
|  |         // the NotificationChannel class is new and not in the support library | ||||||
|  |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||||
|  |             CharSequence name = getString(R.string.app_notification_channel_name); | ||||||
|  |             String description = getString(R.string.app_notification_channel_description); | ||||||
|  |             NotificationChannel channel = new NotificationChannel(getString(R.string.app_notification_channel_id), name, NotificationManager.IMPORTANCE_LOW); | ||||||
|  |             channel.setDescription(description); | ||||||
|  |             channel.setSound(null, null); | ||||||
|  |             channel.setVibrationPattern(null); | ||||||
|  |             // Register the channel with the system; you can't change the importance | ||||||
|  |             // or other notification behaviors after this | ||||||
|  |             NotificationManager notificationManager = getSystemService(NotificationManager.class); | ||||||
|  |             notificationManager.createNotificationChannel(channel); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onCreate() { | ||||||
|  |         super.onCreate(); | ||||||
|  |         application = this; | ||||||
|  | 
 | ||||||
|  |         if (PermissionsHandler.hasWriteAccess(getApplicationContext())) { | ||||||
|  |             DirectoryInitialization.start(getApplicationContext()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         NativeLibrary.LogDeviceInfo(); | ||||||
|  |         createNotificationChannel(); | ||||||
|  | 
 | ||||||
|  |         databaseHelper = new GameDatabase(this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static Context getAppContext() { | ||||||
|  |         return application.getApplicationContext(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,666 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright 2013 Dolphin Emulator Project | ||||||
|  |  * Licensed under GPLv2+ | ||||||
|  |  * Refer to the license.txt file included. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu; | ||||||
|  | 
 | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.app.Dialog; | ||||||
|  | import android.content.pm.PackageManager; | ||||||
|  | import android.content.res.Configuration; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.text.Html; | ||||||
|  | import android.text.method.LinkMovementMethod; | ||||||
|  | import android.view.Surface; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.widget.EditText; | ||||||
|  | import android.widget.FrameLayout; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | import androidx.core.content.ContextCompat; | ||||||
|  | import androidx.fragment.app.DialogFragment; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.activities.EmulationActivity; | ||||||
|  | import org.citra.citra_emu.applets.SoftwareKeyboard; | ||||||
|  | import org.citra.citra_emu.utils.EmulationMenuSettings; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | import org.citra.citra_emu.utils.PermissionsHandler; | ||||||
|  | 
 | ||||||
|  | import java.lang.ref.WeakReference; | ||||||
|  | import java.util.Date; | ||||||
|  | import java.util.Objects; | ||||||
|  | 
 | ||||||
|  | import static android.Manifest.permission.CAMERA; | ||||||
|  | import static android.Manifest.permission.RECORD_AUDIO; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Class which contains methods that interact | ||||||
|  |  * with the native side of the Citra code. | ||||||
|  |  */ | ||||||
|  | public final class NativeLibrary { | ||||||
|  |     /** | ||||||
|  |      * Default touchscreen device | ||||||
|  |      */ | ||||||
|  |     public static final String TouchScreenDevice = "Touchscreen"; | ||||||
|  |     public static WeakReference<EmulationActivity> sEmulationActivity = new WeakReference<>(null); | ||||||
|  | 
 | ||||||
|  |     private static boolean alertResult = false; | ||||||
|  |     private static String alertPromptResult = ""; | ||||||
|  |     private static int alertPromptButton = 0; | ||||||
|  |     private static final Object alertPromptLock = new Object(); | ||||||
|  |     private static boolean alertPromptInProgress = false; | ||||||
|  |     private static String alertPromptCaption = ""; | ||||||
|  |     private static int alertPromptButtonConfig = 0; | ||||||
|  |     private static EditText alertPromptEditText = null; | ||||||
|  | 
 | ||||||
|  |     static { | ||||||
|  |         try { | ||||||
|  |             System.loadLibrary("citra-android"); | ||||||
|  |         } catch (UnsatisfiedLinkError ex) { | ||||||
|  |             Log.error("[NativeLibrary] " + ex.toString()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private NativeLibrary() { | ||||||
|  |         // Disallows instantiation. | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles button press events for a gamepad. | ||||||
|  |      * | ||||||
|  |      * @param Device The input descriptor of the gamepad. | ||||||
|  |      * @param Button Key code identifying which button was pressed. | ||||||
|  |      * @param Action Mask identifying which action is happening (button pressed down, or button released). | ||||||
|  |      * @return If we handled the button press. | ||||||
|  |      */ | ||||||
|  |     public static native boolean onGamePadEvent(String Device, int Button, int Action); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles gamepad movement events. | ||||||
|  |      * | ||||||
|  |      * @param Device The device ID of the gamepad. | ||||||
|  |      * @param Axis   The axis ID | ||||||
|  |      * @param x_axis The value of the x-axis represented by the given ID. | ||||||
|  |      * @param y_axis The value of the y-axis represented by the given ID | ||||||
|  |      */ | ||||||
|  |     public static native boolean onGamePadMoveEvent(String Device, int Axis, float x_axis, float y_axis); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles gamepad movement events. | ||||||
|  |      * | ||||||
|  |      * @param Device   The device ID of the gamepad. | ||||||
|  |      * @param Axis_id  The axis ID | ||||||
|  |      * @param axis_val The value of the axis represented by the given ID. | ||||||
|  |      */ | ||||||
|  |     public static native boolean onGamePadAxisEvent(String Device, int Axis_id, float axis_val); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles touch events. | ||||||
|  |      * | ||||||
|  |      * @param x_axis  The value of the x-axis. | ||||||
|  |      * @param y_axis  The value of the y-axis | ||||||
|  |      * @param pressed To identify if the touch held down or released. | ||||||
|  |      * @return true if the pointer is within the touchscreen | ||||||
|  |      */ | ||||||
|  |     public static native boolean onTouchEvent(float x_axis, float y_axis, boolean pressed); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles touch movement. | ||||||
|  |      * | ||||||
|  |      * @param x_axis The value of the instantaneous x-axis. | ||||||
|  |      * @param y_axis The value of the instantaneous y-axis. | ||||||
|  |      */ | ||||||
|  |     public static native void onTouchMoved(float x_axis, float y_axis); | ||||||
|  | 
 | ||||||
|  |     public static native void ReloadSettings(); | ||||||
|  | 
 | ||||||
|  |     public static native String GetUserSetting(String gameID, String Section, String Key); | ||||||
|  | 
 | ||||||
|  |     public static native void SetUserSetting(String gameID, String Section, String Key, String Value); | ||||||
|  | 
 | ||||||
|  |     public static native void InitGameIni(String gameID); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets the embedded icon within the given ROM. | ||||||
|  |      * | ||||||
|  |      * @param filename the file path to the ROM. | ||||||
|  |      * @return an integer array containing the color data for the icon. | ||||||
|  |      */ | ||||||
|  |     public static native int[] GetIcon(String filename); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets the embedded title of the given ISO/ROM. | ||||||
|  |      * | ||||||
|  |      * @param filename The file path to the ISO/ROM. | ||||||
|  |      * @return the embedded title of the ISO/ROM. | ||||||
|  |      */ | ||||||
|  |     public static native String GetTitle(String filename); | ||||||
|  | 
 | ||||||
|  |     public static native String GetDescription(String filename); | ||||||
|  | 
 | ||||||
|  |     public static native String GetGameId(String filename); | ||||||
|  | 
 | ||||||
|  |     public static native String GetRegions(String filename); | ||||||
|  | 
 | ||||||
|  |     public static native String GetCompany(String filename); | ||||||
|  | 
 | ||||||
|  |     public static native String GetGitRevision(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sets the current working user directory | ||||||
|  |      * If not set, it auto-detects a location | ||||||
|  |      */ | ||||||
|  |     public static native void SetUserDirectory(String directory); | ||||||
|  | 
 | ||||||
|  |     public static native String[] GetInstalledGamePaths(); | ||||||
|  | 
 | ||||||
|  |     // Create the config.ini file. | ||||||
|  |     public static native void CreateConfigFile(); | ||||||
|  | 
 | ||||||
|  |     public static native int DefaultCPUCore(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Begins emulation. | ||||||
|  |      */ | ||||||
|  |     public static native void Run(String path); | ||||||
|  | 
 | ||||||
|  |     public static native String[] GetTextureFilterNames(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Begins emulation from the specified savestate. | ||||||
|  |      */ | ||||||
|  |     public static native void Run(String path, String savestatePath, boolean deleteSavestate); | ||||||
|  | 
 | ||||||
|  |     // Surface Handling | ||||||
|  |     public static native void SurfaceChanged(Surface surf); | ||||||
|  | 
 | ||||||
|  |     public static native void SurfaceDestroyed(); | ||||||
|  | 
 | ||||||
|  |     public static native void DoFrame(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Unpauses emulation from a paused state. | ||||||
|  |      */ | ||||||
|  |     public static native void UnPauseEmulation(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Pauses emulation. | ||||||
|  |      */ | ||||||
|  |     public static native void PauseEmulation(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Stops emulation. | ||||||
|  |      */ | ||||||
|  |     public static native void StopEmulation(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns true if emulation is running (or is paused). | ||||||
|  |      */ | ||||||
|  |     public static native boolean IsRunning(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns the performance stats for the current game | ||||||
|  |      **/ | ||||||
|  |     public static native double[] GetPerfStats(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Notifies the core emulation that the orientation has changed. | ||||||
|  |      */ | ||||||
|  |     public static native void NotifyOrientationChange(int layout_option, int rotation); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Swaps the top and bottom screens. | ||||||
|  |      */ | ||||||
|  |     public static native void SwapScreens(boolean swap_screens, int rotation); | ||||||
|  | 
 | ||||||
|  |     public enum CoreError { | ||||||
|  |         ErrorSystemFiles, | ||||||
|  |         ErrorSavestate, | ||||||
|  |         ErrorUnknown, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static boolean coreErrorAlertResult = false; | ||||||
|  |     private static final Object coreErrorAlertLock = new Object(); | ||||||
|  | 
 | ||||||
|  |     public static class CoreErrorDialogFragment extends DialogFragment { | ||||||
|  |         static CoreErrorDialogFragment newInstance(String title, String message) { | ||||||
|  |             CoreErrorDialogFragment frag = new CoreErrorDialogFragment(); | ||||||
|  |             Bundle args = new Bundle(); | ||||||
|  |             args.putString("title", title); | ||||||
|  |             args.putString("message", message); | ||||||
|  |             frag.setArguments(args); | ||||||
|  |             return frag; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         @Override | ||||||
|  |         public Dialog onCreateDialog(Bundle savedInstanceState) { | ||||||
|  |             final Activity emulationActivity = Objects.requireNonNull(getActivity()); | ||||||
|  | 
 | ||||||
|  |             final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title")); | ||||||
|  |             final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message")); | ||||||
|  | 
 | ||||||
|  |             return new AlertDialog.Builder(emulationActivity) | ||||||
|  |                     .setTitle(title) | ||||||
|  |                     .setMessage(message) | ||||||
|  |                     .setPositiveButton(R.string.continue_button, (dialog, which) -> { | ||||||
|  |                         coreErrorAlertResult = true; | ||||||
|  |                         synchronized (coreErrorAlertLock) { | ||||||
|  |                             coreErrorAlertLock.notify(); | ||||||
|  |                         } | ||||||
|  |                     }) | ||||||
|  |                     .setNegativeButton(R.string.abort_button, (dialog, which) -> { | ||||||
|  |                         coreErrorAlertResult = false; | ||||||
|  |                         synchronized (coreErrorAlertLock) { | ||||||
|  |                             coreErrorAlertLock.notify(); | ||||||
|  |                         } | ||||||
|  |                     }).setOnDismissListener(dialog -> { | ||||||
|  |                 coreErrorAlertResult = true; | ||||||
|  |                 synchronized (coreErrorAlertLock) { | ||||||
|  |                     coreErrorAlertLock.notify(); | ||||||
|  |                 } | ||||||
|  |             }).create(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void OnCoreErrorImpl(String title, String message) { | ||||||
|  |         final EmulationActivity emulationActivity = sEmulationActivity.get(); | ||||||
|  |         if (emulationActivity == null) { | ||||||
|  |             Log.error("[NativeLibrary] EmulationActivity not present"); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         CoreErrorDialogFragment fragment = CoreErrorDialogFragment.newInstance(title, message); | ||||||
|  |         fragment.show(emulationActivity.getSupportFragmentManager(), "coreError"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handles a core error. | ||||||
|  |      * @return true: continue; false: abort | ||||||
|  |      */ | ||||||
|  |     public static boolean OnCoreError(CoreError error, String details) { | ||||||
|  |         final EmulationActivity emulationActivity = sEmulationActivity.get(); | ||||||
|  |         if (emulationActivity == null) { | ||||||
|  |             Log.error("[NativeLibrary] EmulationActivity not present"); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         String title, message; | ||||||
|  |         switch (error) { | ||||||
|  |             case ErrorSystemFiles: { | ||||||
|  |                 title = emulationActivity.getString(R.string.system_archive_not_found); | ||||||
|  |                 message = emulationActivity.getString(R.string.system_archive_not_found_message, details.isEmpty() ? emulationActivity.getString(R.string.system_archive_general) : details); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             case ErrorSavestate: { | ||||||
|  |                 title = emulationActivity.getString(R.string.save_load_error); | ||||||
|  |                 message = details; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             case ErrorUnknown: { | ||||||
|  |                 title = emulationActivity.getString(R.string.fatal_error); | ||||||
|  |                 message = emulationActivity.getString(R.string.fatal_error_message); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             default: { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Show the AlertDialog on the main thread. | ||||||
|  |         emulationActivity.runOnUiThread(() -> OnCoreErrorImpl(title, message)); | ||||||
|  | 
 | ||||||
|  |         // Wait for the lock to notify that it is complete. | ||||||
|  |         synchronized (coreErrorAlertLock) { | ||||||
|  |             try { | ||||||
|  |                 coreErrorAlertLock.wait(); | ||||||
|  |             } catch (Exception ignored) { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return coreErrorAlertResult; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static boolean isPortraitMode() { | ||||||
|  |         return CitraApplication.getAppContext().getResources().getConfiguration().orientation == | ||||||
|  |                 Configuration.ORIENTATION_PORTRAIT; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static int landscapeScreenLayout() { | ||||||
|  |         return EmulationMenuSettings.getLandscapeScreenLayout(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static boolean displayAlertMsg(final String caption, final String text, | ||||||
|  |                                           final boolean yesNo) { | ||||||
|  |         Log.error("[NativeLibrary] Alert: " + text); | ||||||
|  |         final EmulationActivity emulationActivity = sEmulationActivity.get(); | ||||||
|  |         boolean result = false; | ||||||
|  |         if (emulationActivity == null) { | ||||||
|  |             Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert."); | ||||||
|  |         } else { | ||||||
|  |             // Create object used for waiting. | ||||||
|  |             final Object lock = new Object(); | ||||||
|  |             AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) | ||||||
|  |                     .setTitle(caption) | ||||||
|  |                     .setMessage(text); | ||||||
|  | 
 | ||||||
|  |             // If not yes/no dialog just have one button that dismisses modal, | ||||||
|  |             // otherwise have a yes and no button that sets alertResult accordingly. | ||||||
|  |             if (!yesNo) { | ||||||
|  |                 builder | ||||||
|  |                         .setCancelable(false) | ||||||
|  |                         .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> | ||||||
|  |                         { | ||||||
|  |                             dialog.dismiss(); | ||||||
|  |                             synchronized (lock) { | ||||||
|  |                                 lock.notify(); | ||||||
|  |                             } | ||||||
|  |                         }); | ||||||
|  |             } else { | ||||||
|  |                 alertResult = false; | ||||||
|  | 
 | ||||||
|  |                 builder | ||||||
|  |                         .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> | ||||||
|  |                         { | ||||||
|  |                             alertResult = true; | ||||||
|  |                             dialog.dismiss(); | ||||||
|  |                             synchronized (lock) { | ||||||
|  |                                 lock.notify(); | ||||||
|  |                             } | ||||||
|  |                         }) | ||||||
|  |                         .setNegativeButton(android.R.string.no, (dialog, whichButton) -> | ||||||
|  |                         { | ||||||
|  |                             alertResult = false; | ||||||
|  |                             dialog.dismiss(); | ||||||
|  |                             synchronized (lock) { | ||||||
|  |                                 lock.notify(); | ||||||
|  |                             } | ||||||
|  |                         }); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Show the AlertDialog on the main thread. | ||||||
|  |             emulationActivity.runOnUiThread(builder::show); | ||||||
|  | 
 | ||||||
|  |             // Wait for the lock to notify that it is complete. | ||||||
|  |             synchronized (lock) { | ||||||
|  |                 try { | ||||||
|  |                     lock.wait(); | ||||||
|  |                 } catch (Exception e) { | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (yesNo) | ||||||
|  |                 result = alertResult; | ||||||
|  |         } | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void retryDisplayAlertPrompt() { | ||||||
|  |         if (!alertPromptInProgress) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         displayAlertPromptImpl(alertPromptCaption, alertPromptEditText.getText().toString(), alertPromptButtonConfig).show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static String displayAlertPrompt(String caption, String text, int buttonConfig) { | ||||||
|  |         alertPromptCaption = caption; | ||||||
|  |         alertPromptButtonConfig = buttonConfig; | ||||||
|  |         alertPromptInProgress = true; | ||||||
|  | 
 | ||||||
|  |         // Show the AlertDialog on the main thread | ||||||
|  |         sEmulationActivity.get().runOnUiThread(() -> displayAlertPromptImpl(alertPromptCaption, text, alertPromptButtonConfig).show()); | ||||||
|  | 
 | ||||||
|  |         // Wait for the lock to notify that it is complete | ||||||
|  |         synchronized (alertPromptLock) { | ||||||
|  |             try { | ||||||
|  |                 alertPromptLock.wait(); | ||||||
|  |             } catch (Exception e) { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         alertPromptInProgress = false; | ||||||
|  | 
 | ||||||
|  |         return alertPromptResult; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static AlertDialog.Builder displayAlertPromptImpl(String caption, String text, int buttonConfig) { | ||||||
|  |         final EmulationActivity emulationActivity = sEmulationActivity.get(); | ||||||
|  |         alertPromptResult = ""; | ||||||
|  |         alertPromptButton = 0; | ||||||
|  | 
 | ||||||
|  |         FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); | ||||||
|  |         params.leftMargin = params.rightMargin = CitraApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.dialog_margin); | ||||||
|  | 
 | ||||||
|  |         // Set up the input | ||||||
|  |         alertPromptEditText = new EditText(CitraApplication.getAppContext()); | ||||||
|  |         alertPromptEditText.setText(text); | ||||||
|  |         alertPromptEditText.setSingleLine(); | ||||||
|  |         alertPromptEditText.setLayoutParams(params); | ||||||
|  | 
 | ||||||
|  |         FrameLayout container = new FrameLayout(emulationActivity); | ||||||
|  |         container.addView(alertPromptEditText); | ||||||
|  | 
 | ||||||
|  |         AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) | ||||||
|  |                 .setTitle(caption) | ||||||
|  |                 .setView(container) | ||||||
|  |                 .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> | ||||||
|  |                 { | ||||||
|  |                     alertPromptButton = buttonConfig; | ||||||
|  |                     alertPromptResult = alertPromptEditText.getText().toString(); | ||||||
|  |                     synchronized (alertPromptLock) { | ||||||
|  |                         alertPromptLock.notifyAll(); | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |                 .setOnDismissListener(dialogInterface -> | ||||||
|  |                 { | ||||||
|  |                     alertPromptResult = ""; | ||||||
|  |                     synchronized (alertPromptLock) { | ||||||
|  |                         alertPromptLock.notifyAll(); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |         if (buttonConfig > 0) { | ||||||
|  |             builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> | ||||||
|  |             { | ||||||
|  |                 alertPromptResult = ""; | ||||||
|  |                 synchronized (alertPromptLock) { | ||||||
|  |                     alertPromptLock.notifyAll(); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return builder; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static int alertPromptButton() { | ||||||
|  |         return alertPromptButton; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void exitEmulationActivity(int resultCode) { | ||||||
|  |         final int Success = 0; | ||||||
|  |         final int ErrorNotInitialized = 1; | ||||||
|  |         final int ErrorGetLoader = 2; | ||||||
|  |         final int ErrorSystemMode = 3; | ||||||
|  |         final int ErrorLoader = 4; | ||||||
|  |         final int ErrorLoader_ErrorEncrypted = 5; | ||||||
|  |         final int ErrorLoader_ErrorInvalidFormat = 6; | ||||||
|  |         final int ErrorSystemFiles = 7; | ||||||
|  |         final int ErrorVideoCore = 8; | ||||||
|  |         final int ErrorVideoCore_ErrorGenericDrivers = 9; | ||||||
|  |         final int ErrorVideoCore_ErrorBelowGL33 = 10; | ||||||
|  |         final int ShutdownRequested = 11; | ||||||
|  |         final int ErrorUnknown = 12; | ||||||
|  | 
 | ||||||
|  |         final EmulationActivity emulationActivity = sEmulationActivity.get(); | ||||||
|  |         if (emulationActivity == null) { | ||||||
|  |             Log.warning("[NativeLibrary] EmulationActivity is null, can't exit."); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         int captionId = R.string.loader_error_invalid_format; | ||||||
|  |         if (resultCode == ErrorLoader_ErrorEncrypted) { | ||||||
|  |             captionId = R.string.loader_error_encrypted; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) | ||||||
|  |                 .setTitle(captionId) | ||||||
|  |                 .setMessage(Html.fromHtml("Please follow the guides to redump your <a href=\"https://citra-emu.org/wiki/dumping-game-cartridges/\">game cartidges</a> or <a href=\"https://citra-emu.org/wiki/dumping-installed-titles/\">installed titles</a>.", Html.FROM_HTML_MODE_LEGACY)) | ||||||
|  |                 .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> emulationActivity.finish()) | ||||||
|  |                 .setOnDismissListener(dialogInterface -> emulationActivity.finish()); | ||||||
|  |         emulationActivity.runOnUiThread(() -> { | ||||||
|  |             AlertDialog alert = builder.create(); | ||||||
|  |             alert.show(); | ||||||
|  |             ((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void setEmulationActivity(EmulationActivity emulationActivity) { | ||||||
|  |         Log.verbose("[NativeLibrary] Registering EmulationActivity."); | ||||||
|  |         sEmulationActivity = new WeakReference<>(emulationActivity); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void clearEmulationActivity() { | ||||||
|  |         Log.verbose("[NativeLibrary] Unregistering EmulationActivity."); | ||||||
|  | 
 | ||||||
|  |         sEmulationActivity.clear(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static final Object cameraPermissionLock = new Object(); | ||||||
|  |     private static boolean cameraPermissionGranted = false; | ||||||
|  |     public static final int REQUEST_CODE_NATIVE_CAMERA = 800; | ||||||
|  | 
 | ||||||
|  |     public static boolean RequestCameraPermission() { | ||||||
|  |         final EmulationActivity emulationActivity = sEmulationActivity.get(); | ||||||
|  |         if (emulationActivity == null) { | ||||||
|  |             Log.error("[NativeLibrary] EmulationActivity not present"); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         if (ContextCompat.checkSelfPermission(emulationActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) { | ||||||
|  |             // Permission already granted | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         emulationActivity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_NATIVE_CAMERA); | ||||||
|  | 
 | ||||||
|  |         // Wait until result is returned | ||||||
|  |         synchronized (cameraPermissionLock) { | ||||||
|  |             try { | ||||||
|  |                 cameraPermissionLock.wait(); | ||||||
|  |             } catch (InterruptedException ignored) { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return cameraPermissionGranted; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void CameraPermissionResult(boolean granted) { | ||||||
|  |         cameraPermissionGranted = granted; | ||||||
|  |         synchronized (cameraPermissionLock) { | ||||||
|  |             cameraPermissionLock.notify(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static final Object micPermissionLock = new Object(); | ||||||
|  |     private static boolean micPermissionGranted = false; | ||||||
|  |     public static final int REQUEST_CODE_NATIVE_MIC = 900; | ||||||
|  | 
 | ||||||
|  |     public static boolean RequestMicPermission() { | ||||||
|  |         final EmulationActivity emulationActivity = sEmulationActivity.get(); | ||||||
|  |         if (emulationActivity == null) { | ||||||
|  |             Log.error("[NativeLibrary] EmulationActivity not present"); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { | ||||||
|  |             // Permission already granted | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC); | ||||||
|  | 
 | ||||||
|  |         // Wait until result is returned | ||||||
|  |         synchronized (micPermissionLock) { | ||||||
|  |             try { | ||||||
|  |                 micPermissionLock.wait(); | ||||||
|  |             } catch (InterruptedException ignored) { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return micPermissionGranted; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void MicPermissionResult(boolean granted) { | ||||||
|  |         micPermissionGranted = granted; | ||||||
|  |         synchronized (micPermissionLock) { | ||||||
|  |             micPermissionLock.notify(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Notifies that the activity is now in foreground and camera devices can now be reloaded | ||||||
|  |     public static native void ReloadCameraDevices(); | ||||||
|  | 
 | ||||||
|  |     public static native boolean LoadAmiibo(byte[] bytes); | ||||||
|  | 
 | ||||||
|  |     public static native void RemoveAmiibo(); | ||||||
|  | 
 | ||||||
|  |     public static native void InstallCIAS(String[] path); | ||||||
|  | 
 | ||||||
|  |     public static final int SAVESTATE_SLOT_COUNT = 10; | ||||||
|  | 
 | ||||||
|  |     public static final class SavestateInfo { | ||||||
|  |         public int slot; | ||||||
|  |         public Date time; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public static native SavestateInfo[] GetSavestateInfo(); | ||||||
|  | 
 | ||||||
|  |     public static native void SaveState(int slot); | ||||||
|  |     public static native void LoadState(int slot); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Logs the Citra version, Android version and, CPU. | ||||||
|  |      */ | ||||||
|  |     public static native void LogDeviceInfo(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Button type for use in onTouchEvent | ||||||
|  |      */ | ||||||
|  |     public static final class ButtonType { | ||||||
|  |         public static final int BUTTON_A = 700; | ||||||
|  |         public static final int BUTTON_B = 701; | ||||||
|  |         public static final int BUTTON_X = 702; | ||||||
|  |         public static final int BUTTON_Y = 703; | ||||||
|  |         public static final int BUTTON_START = 704; | ||||||
|  |         public static final int BUTTON_SELECT = 705; | ||||||
|  |         public static final int BUTTON_HOME = 706; | ||||||
|  |         public static final int BUTTON_ZL = 707; | ||||||
|  |         public static final int BUTTON_ZR = 708; | ||||||
|  |         public static final int DPAD_UP = 709; | ||||||
|  |         public static final int DPAD_DOWN = 710; | ||||||
|  |         public static final int DPAD_LEFT = 711; | ||||||
|  |         public static final int DPAD_RIGHT = 712; | ||||||
|  |         public static final int STICK_LEFT = 713; | ||||||
|  |         public static final int STICK_LEFT_UP = 714; | ||||||
|  |         public static final int STICK_LEFT_DOWN = 715; | ||||||
|  |         public static final int STICK_LEFT_LEFT = 716; | ||||||
|  |         public static final int STICK_LEFT_RIGHT = 717; | ||||||
|  |         public static final int STICK_C = 718; | ||||||
|  |         public static final int STICK_C_UP = 719; | ||||||
|  |         public static final int STICK_C_DOWN = 720; | ||||||
|  |         public static final int STICK_C_LEFT = 771; | ||||||
|  |         public static final int STICK_C_RIGHT = 772; | ||||||
|  |         public static final int TRIGGER_L = 773; | ||||||
|  |         public static final int TRIGGER_R = 774; | ||||||
|  |         public static final int DPAD = 780; | ||||||
|  |         public static final int BUTTON_DEBUG = 781; | ||||||
|  |         public static final int BUTTON_GPIO14 = 782; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Button states | ||||||
|  |      */ | ||||||
|  |     public static final class ButtonState { | ||||||
|  |         public static final int RELEASED = 0; | ||||||
|  |         public static final int PRESSED = 1; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | package org.citra.citra_emu.activities; | ||||||
|  | 
 | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.os.Environment; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | import com.nononsenseapps.filepicker.AbstractFilePickerFragment; | ||||||
|  | import com.nononsenseapps.filepicker.FilePickerActivity; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.fragments.CustomFilePickerFragment; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | 
 | ||||||
|  | public class CustomFilePickerActivity extends FilePickerActivity { | ||||||
|  |     public static final String EXTRA_TITLE = "filepicker.intent.TITLE"; | ||||||
|  |     public static final String EXTRA_EXTENSIONS = "filepicker.intent.EXTENSIONS"; | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected AbstractFilePickerFragment<File> getFragment( | ||||||
|  |             @Nullable final String startPath, final int mode, final boolean allowMultiple, | ||||||
|  |             final boolean allowCreateDir, final boolean allowExistingFile, | ||||||
|  |             final boolean singleClick) { | ||||||
|  |         CustomFilePickerFragment fragment = new CustomFilePickerFragment(); | ||||||
|  |         // startPath is allowed to be null. In that case, default folder should be SD-card and not "/" | ||||||
|  |         fragment.setArgs( | ||||||
|  |                 startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(), | ||||||
|  |                 mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick); | ||||||
|  | 
 | ||||||
|  |         Intent intent = getIntent(); | ||||||
|  |         int title = intent == null ? 0 : intent.getIntExtra(EXTRA_TITLE, 0); | ||||||
|  |         fragment.setTitle(title); | ||||||
|  |         String allowedExtensions = intent == null ? "*" : intent.getStringExtra(EXTRA_EXTENSIONS); | ||||||
|  |         fragment.setAllowedExtensions(allowedExtensions); | ||||||
|  | 
 | ||||||
|  |         return fragment; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,788 @@ | ||||||
|  | package org.citra.citra_emu.activities; | ||||||
|  | 
 | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.content.pm.PackageManager; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.os.Handler; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | import android.util.SparseIntArray; | ||||||
|  | import android.view.InputDevice; | ||||||
|  | import android.view.KeyEvent; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.Menu; | ||||||
|  | import android.view.MenuItem; | ||||||
|  | import android.view.MotionEvent; | ||||||
|  | import android.view.SubMenu; | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.CheckBox; | ||||||
|  | import android.widget.SeekBar; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.IntDef; | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | import androidx.appcompat.app.AppCompatActivity; | ||||||
|  | import androidx.core.app.NotificationManagerCompat; | ||||||
|  | import androidx.fragment.app.FragmentActivity; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsActivity; | ||||||
|  | import org.citra.citra_emu.features.settings.utils.SettingsFile; | ||||||
|  | import org.citra.citra_emu.camera.StillImageCameraHelper; | ||||||
|  | import org.citra.citra_emu.fragments.EmulationFragment; | ||||||
|  | import org.citra.citra_emu.ui.main.MainActivity; | ||||||
|  | import org.citra.citra_emu.utils.ControllerMappingHelper; | ||||||
|  | import org.citra.citra_emu.utils.EmulationMenuSettings; | ||||||
|  | import org.citra.citra_emu.utils.FileBrowserHelper; | ||||||
|  | import org.citra.citra_emu.utils.FileUtil; | ||||||
|  | import org.citra.citra_emu.utils.ForegroundService; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.lang.annotation.Retention; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import static android.Manifest.permission.CAMERA; | ||||||
|  | import static android.Manifest.permission.RECORD_AUDIO; | ||||||
|  | import static java.lang.annotation.RetentionPolicy.SOURCE; | ||||||
|  | 
 | ||||||
|  | public final class EmulationActivity extends AppCompatActivity { | ||||||
|  |     public static final String EXTRA_SELECTED_GAME = "SelectedGame"; | ||||||
|  |     public static final String EXTRA_SELECTED_TITLE = "SelectedTitle"; | ||||||
|  |     public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0; | ||||||
|  |     public static final int MENU_ACTION_TOGGLE_CONTROLS = 1; | ||||||
|  |     public static final int MENU_ACTION_ADJUST_SCALE = 2; | ||||||
|  |     public static final int MENU_ACTION_EXIT = 3; | ||||||
|  |     public static final int MENU_ACTION_SHOW_FPS = 4; | ||||||
|  |     public static final int MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE = 5; | ||||||
|  |     public static final int MENU_ACTION_SCREEN_LAYOUT_PORTRAIT = 6; | ||||||
|  |     public static final int MENU_ACTION_SCREEN_LAYOUT_SINGLE = 7; | ||||||
|  |     public static final int MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE = 8; | ||||||
|  |     public static final int MENU_ACTION_SWAP_SCREENS = 9; | ||||||
|  |     public static final int MENU_ACTION_RESET_OVERLAY = 10; | ||||||
|  |     public static final int MENU_ACTION_SHOW_OVERLAY = 11; | ||||||
|  |     public static final int MENU_ACTION_OPEN_SETTINGS = 12; | ||||||
|  |     public static final int MENU_ACTION_LOAD_AMIIBO = 13; | ||||||
|  |     public static final int MENU_ACTION_REMOVE_AMIIBO = 14; | ||||||
|  |     public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15; | ||||||
|  |     public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16; | ||||||
|  | 
 | ||||||
|  |     public static final int REQUEST_SELECT_AMIIBO = 2; | ||||||
|  |     private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000; | ||||||
|  |     private static SparseIntArray buttonsActionsMap = new SparseIntArray(); | ||||||
|  | 
 | ||||||
|  |     static { | ||||||
|  |         buttonsActionsMap.append(R.id.menu_emulation_edit_layout, | ||||||
|  |                 EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT); | ||||||
|  |         buttonsActionsMap.append(R.id.menu_emulation_toggle_controls, | ||||||
|  |                 EmulationActivity.MENU_ACTION_TOGGLE_CONTROLS); | ||||||
|  |         buttonsActionsMap | ||||||
|  |                 .append(R.id.menu_emulation_adjust_scale, EmulationActivity.MENU_ACTION_ADJUST_SCALE); | ||||||
|  |         buttonsActionsMap.append(R.id.menu_emulation_show_fps, | ||||||
|  |                 EmulationActivity.MENU_ACTION_SHOW_FPS); | ||||||
|  |         buttonsActionsMap.append(R.id.menu_screen_layout_landscape, | ||||||
|  |                 EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE); | ||||||
|  |         buttonsActionsMap.append(R.id.menu_screen_layout_portrait, | ||||||
|  |                 EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_PORTRAIT); | ||||||
|  |         buttonsActionsMap.append(R.id.menu_screen_layout_single, | ||||||
|  |                 EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SINGLE); | ||||||
|  |         buttonsActionsMap.append(R.id.menu_screen_layout_sidebyside, | ||||||
|  |                 EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE); | ||||||
|  |         buttonsActionsMap.append(R.id.menu_emulation_swap_screens, | ||||||
|  |                 EmulationActivity.MENU_ACTION_SWAP_SCREENS); | ||||||
|  |         buttonsActionsMap | ||||||
|  |                 .append(R.id.menu_emulation_reset_overlay, EmulationActivity.MENU_ACTION_RESET_OVERLAY); | ||||||
|  |         buttonsActionsMap | ||||||
|  |                 .append(R.id.menu_emulation_show_overlay, EmulationActivity.MENU_ACTION_SHOW_OVERLAY); | ||||||
|  |         buttonsActionsMap | ||||||
|  |                 .append(R.id.menu_emulation_open_settings, EmulationActivity.MENU_ACTION_OPEN_SETTINGS); | ||||||
|  |         buttonsActionsMap | ||||||
|  |                 .append(R.id.menu_emulation_amiibo_load, EmulationActivity.MENU_ACTION_LOAD_AMIIBO); | ||||||
|  |         buttonsActionsMap | ||||||
|  |                 .append(R.id.menu_emulation_amiibo_remove, EmulationActivity.MENU_ACTION_REMOVE_AMIIBO); | ||||||
|  |         buttonsActionsMap.append(R.id.menu_emulation_joystick_rel_center, | ||||||
|  |                 EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER); | ||||||
|  |         buttonsActionsMap.append(R.id.menu_emulation_dpad_slide_enable, | ||||||
|  |                 EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private View mDecorView; | ||||||
|  |     private EmulationFragment mEmulationFragment; | ||||||
|  |     private SharedPreferences mPreferences; | ||||||
|  |     private ControllerMappingHelper mControllerMappingHelper; | ||||||
|  |     private Intent foregroundService; | ||||||
|  |     private boolean activityRecreated; | ||||||
|  |     private String mSelectedTitle; | ||||||
|  |     private String mPath; | ||||||
|  | 
 | ||||||
|  |     public static void launch(FragmentActivity activity, String path, String title) { | ||||||
|  |         Intent launcher = new Intent(activity, EmulationActivity.class); | ||||||
|  | 
 | ||||||
|  |         launcher.putExtra(EXTRA_SELECTED_GAME, path); | ||||||
|  |         launcher.putExtra(EXTRA_SELECTED_TITLE, title); | ||||||
|  |         activity.startActivity(launcher); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void tryDismissRunningNotification(Activity activity) { | ||||||
|  |         NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onDestroy() { | ||||||
|  |         stopService(foregroundService); | ||||||
|  |         super.onDestroy(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  | 
 | ||||||
|  |         if (savedInstanceState == null) { | ||||||
|  |             // Get params we were passed | ||||||
|  |             Intent gameToEmulate = getIntent(); | ||||||
|  |             mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME); | ||||||
|  |             mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE); | ||||||
|  |             activityRecreated = false; | ||||||
|  |         } else { | ||||||
|  |             activityRecreated = true; | ||||||
|  |             restoreState(savedInstanceState); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         mControllerMappingHelper = new ControllerMappingHelper(); | ||||||
|  | 
 | ||||||
|  |         // Get a handle to the Window containing the UI. | ||||||
|  |         mDecorView = getWindow().getDecorView(); | ||||||
|  |         mDecorView.setOnSystemUiVisibilityChangeListener(visibility -> | ||||||
|  |         { | ||||||
|  |             if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { | ||||||
|  |                 // Go back to immersive fullscreen mode in 3s | ||||||
|  |                 Handler handler = new Handler(getMainLooper()); | ||||||
|  |                 handler.postDelayed(this::enableFullscreenImmersive, 3000 /* 3s */); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         // Set these options now so that the SurfaceView the game renders into is the right size. | ||||||
|  |         enableFullscreenImmersive(); | ||||||
|  | 
 | ||||||
|  |         setTheme(R.style.CitraEmulationBase); | ||||||
|  | 
 | ||||||
|  |         setContentView(R.layout.activity_emulation); | ||||||
|  | 
 | ||||||
|  |         // Find or create the EmulationFragment | ||||||
|  |         mEmulationFragment = (EmulationFragment) getSupportFragmentManager() | ||||||
|  |                 .findFragmentById(R.id.frame_emulation_fragment); | ||||||
|  |         if (mEmulationFragment == null) { | ||||||
|  |             mEmulationFragment = EmulationFragment.newInstance(mPath); | ||||||
|  |             getSupportFragmentManager().beginTransaction() | ||||||
|  |                     .add(R.id.frame_emulation_fragment, mEmulationFragment) | ||||||
|  |                     .commit(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         setTitle(mSelectedTitle); | ||||||
|  | 
 | ||||||
|  |         mPreferences = PreferenceManager.getDefaultSharedPreferences(this); | ||||||
|  | 
 | ||||||
|  |         // Start a foreground service to prevent the app from getting killed in the background | ||||||
|  |         foregroundService = new Intent(EmulationActivity.this, ForegroundService.class); | ||||||
|  |         startForegroundService(foregroundService); | ||||||
|  | 
 | ||||||
|  |         // Override Citra core INI with the one set by our in game menu | ||||||
|  |         NativeLibrary.SwapScreens(EmulationMenuSettings.getSwapScreens(), | ||||||
|  |                 getWindowManager().getDefaultDisplay().getRotation()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onSaveInstanceState(@NonNull Bundle outState) { | ||||||
|  |         outState.putString(EXTRA_SELECTED_GAME, mPath); | ||||||
|  |         outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle); | ||||||
|  |         super.onSaveInstanceState(outState); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected void restoreState(Bundle savedInstanceState) { | ||||||
|  |         mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME); | ||||||
|  |         mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE); | ||||||
|  | 
 | ||||||
|  |         // If an alert prompt was in progress when state was restored, retry displaying it | ||||||
|  |         NativeLibrary.retryDisplayAlertPrompt(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onRestart() { | ||||||
|  |         super.onRestart(); | ||||||
|  |         NativeLibrary.ReloadCameraDevices(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onBackPressed() { | ||||||
|  |         NativeLibrary.PauseEmulation(); | ||||||
|  |         new AlertDialog.Builder(this) | ||||||
|  |                 .setTitle(R.string.emulation_close_game) | ||||||
|  |                 .setMessage(R.string.emulation_close_game_message) | ||||||
|  |                 .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> | ||||||
|  |                 { | ||||||
|  |                     mEmulationFragment.stopEmulation(); | ||||||
|  |                     finish(); | ||||||
|  |                 }) | ||||||
|  |                 .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> | ||||||
|  |                     NativeLibrary.UnPauseEmulation()) | ||||||
|  |                 .setOnCancelListener(dialogInterface -> | ||||||
|  |                     NativeLibrary.UnPauseEmulation()) | ||||||
|  |                 .create() | ||||||
|  |                 .show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { | ||||||
|  |         switch (requestCode) { | ||||||
|  |             case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA: | ||||||
|  |                 if (grantResults[0] != PackageManager.PERMISSION_GRANTED && | ||||||
|  |                         shouldShowRequestPermissionRationale(CAMERA)) { | ||||||
|  |                     new AlertDialog.Builder(this) | ||||||
|  |                             .setTitle(R.string.camera) | ||||||
|  |                             .setMessage(R.string.camera_permission_needed) | ||||||
|  |                             .setPositiveButton(android.R.string.ok, null) | ||||||
|  |                             .show(); | ||||||
|  |                 } | ||||||
|  |                 NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED); | ||||||
|  |                 break; | ||||||
|  |             case NativeLibrary.REQUEST_CODE_NATIVE_MIC: | ||||||
|  |                 if (grantResults[0] != PackageManager.PERMISSION_GRANTED && | ||||||
|  |                         shouldShowRequestPermissionRationale(RECORD_AUDIO)) { | ||||||
|  |                     new AlertDialog.Builder(this) | ||||||
|  |                             .setTitle(R.string.microphone) | ||||||
|  |                             .setMessage(R.string.microphone_permission_needed) | ||||||
|  |                             .setPositiveButton(android.R.string.ok, null) | ||||||
|  |                             .show(); | ||||||
|  |                 } | ||||||
|  |                 NativeLibrary.MicPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 super.onRequestPermissionsResult(requestCode, permissions, grantResults); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void enableFullscreenImmersive() { | ||||||
|  |         // It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar. | ||||||
|  |         mDecorView.setSystemUiVisibility( | ||||||
|  |                 View.SYSTEM_UI_FLAG_LAYOUT_STABLE | | ||||||
|  |                         View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | | ||||||
|  |                         View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | | ||||||
|  |                         View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | | ||||||
|  |                         View.SYSTEM_UI_FLAG_FULLSCREEN | | ||||||
|  |                         View.SYSTEM_UI_FLAG_IMMERSIVE); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onCreateOptionsMenu(Menu menu) { | ||||||
|  |         // Inflate the menu; this adds items to the action bar if it is present. | ||||||
|  |         getMenuInflater().inflate(R.menu.menu_emulation, menu); | ||||||
|  | 
 | ||||||
|  |         int layoutOptionMenuItem = R.id.menu_screen_layout_landscape; | ||||||
|  |         switch (EmulationMenuSettings.getLandscapeScreenLayout()) { | ||||||
|  |             case EmulationMenuSettings.LayoutOption_SingleScreen: | ||||||
|  |                 layoutOptionMenuItem = R.id.menu_screen_layout_single; | ||||||
|  |                 break; | ||||||
|  |             case EmulationMenuSettings.LayoutOption_SideScreen: | ||||||
|  |                 layoutOptionMenuItem = R.id.menu_screen_layout_sidebyside; | ||||||
|  |                 break; | ||||||
|  |             case EmulationMenuSettings.LayoutOption_MobilePortrait: | ||||||
|  |                 layoutOptionMenuItem = R.id.menu_screen_layout_portrait; | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         menu.findItem(layoutOptionMenuItem).setChecked(true); | ||||||
|  |         menu.findItem(R.id.menu_emulation_joystick_rel_center).setChecked(EmulationMenuSettings.getJoystickRelCenter()); | ||||||
|  |         menu.findItem(R.id.menu_emulation_dpad_slide_enable).setChecked(EmulationMenuSettings.getDpadSlideEnable()); | ||||||
|  |         menu.findItem(R.id.menu_emulation_show_fps).setChecked(EmulationMenuSettings.getShowFps()); | ||||||
|  |         menu.findItem(R.id.menu_emulation_swap_screens).setChecked(EmulationMenuSettings.getSwapScreens()); | ||||||
|  |         menu.findItem(R.id.menu_emulation_show_overlay).setChecked(EmulationMenuSettings.getShowOverlay()); | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void DisplaySavestateWarning() { | ||||||
|  |         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); | ||||||
|  |         if (preferences.getBoolean("savestateWarningShown", false)) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         LayoutInflater inflater = mEmulationFragment.requireActivity().getLayoutInflater(); | ||||||
|  |         View view = inflater.inflate(R.layout.dialog_checkbox, null); | ||||||
|  |         CheckBox checkBox = view.findViewById(R.id.checkBox); | ||||||
|  | 
 | ||||||
|  |         new AlertDialog.Builder(this) | ||||||
|  |                 .setTitle(R.string.savestate_warning_title) | ||||||
|  |                 .setMessage(R.string.savestate_warning_message) | ||||||
|  |                 .setView(view) | ||||||
|  |                 .setPositiveButton(android.R.string.ok, (dialog, which) -> { | ||||||
|  |                     preferences.edit().putBoolean("savestateWarningShown", checkBox.isChecked()).apply(); | ||||||
|  |                 }) | ||||||
|  |                 .show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onPrepareOptionsMenu(Menu menu) { | ||||||
|  |         super.onPrepareOptionsMenu(menu); | ||||||
|  | 
 | ||||||
|  |         final NativeLibrary.SavestateInfo[] savestates = NativeLibrary.GetSavestateInfo(); | ||||||
|  |         if (savestates == null) { | ||||||
|  |             menu.findItem(R.id.menu_emulation_save_state).setVisible(false); | ||||||
|  |             menu.findItem(R.id.menu_emulation_load_state).setVisible(false); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         menu.findItem(R.id.menu_emulation_save_state).setVisible(true); | ||||||
|  |         menu.findItem(R.id.menu_emulation_load_state).setVisible(true); | ||||||
|  | 
 | ||||||
|  |         final SubMenu saveStateMenu = menu.findItem(R.id.menu_emulation_save_state).getSubMenu(); | ||||||
|  |         final SubMenu loadStateMenu = menu.findItem(R.id.menu_emulation_load_state).getSubMenu(); | ||||||
|  |         saveStateMenu.clear(); | ||||||
|  |         loadStateMenu.clear(); | ||||||
|  | 
 | ||||||
|  |         // Update savestates information | ||||||
|  |         for (int i = 0; i < NativeLibrary.SAVESTATE_SLOT_COUNT; ++i) { | ||||||
|  |             final int slot = i + 1; | ||||||
|  |             final String text = getString(R.string.emulation_empty_state_slot, slot); | ||||||
|  |             saveStateMenu.add(text).setEnabled(true).setOnMenuItemClickListener((item) -> { | ||||||
|  |                 DisplaySavestateWarning(); | ||||||
|  |                 NativeLibrary.SaveState(slot); | ||||||
|  |                 return true; | ||||||
|  |             }); | ||||||
|  |             loadStateMenu.add(text).setEnabled(false).setOnMenuItemClickListener((item) -> { | ||||||
|  |                 NativeLibrary.LoadState(slot); | ||||||
|  |                 return true; | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |         for (final NativeLibrary.SavestateInfo info : savestates) { | ||||||
|  |             final String text = getString(R.string.emulation_occupied_state_slot, info.slot, info.time); | ||||||
|  |             saveStateMenu.getItem(info.slot - 1).setTitle(text); | ||||||
|  |             loadStateMenu.getItem(info.slot - 1).setTitle(text).setEnabled(true); | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @SuppressWarnings("WrongConstant") | ||||||
|  |     @Override | ||||||
|  |     public boolean onOptionsItemSelected(MenuItem item) { | ||||||
|  |         int action = buttonsActionsMap.get(item.getItemId(), -1); | ||||||
|  | 
 | ||||||
|  |         switch (action) { | ||||||
|  |             // Edit the placement of the controls | ||||||
|  |             case MENU_ACTION_EDIT_CONTROLS_PLACEMENT: | ||||||
|  |                 editControlsPlacement(); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             // Enable/Disable specific buttons or the entire input overlay. | ||||||
|  |             case MENU_ACTION_TOGGLE_CONTROLS: | ||||||
|  |                 toggleControls(); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             // Adjust the scale of the overlay controls. | ||||||
|  |             case MENU_ACTION_ADJUST_SCALE: | ||||||
|  |                 adjustScale(); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             // Toggle the visibility of the Performance stats TextView | ||||||
|  |             case MENU_ACTION_SHOW_FPS: { | ||||||
|  |                 final boolean isEnabled = !EmulationMenuSettings.getShowFps(); | ||||||
|  |                 EmulationMenuSettings.setShowFps(isEnabled); | ||||||
|  |                 item.setChecked(isEnabled); | ||||||
|  | 
 | ||||||
|  |                 mEmulationFragment.updateShowFpsOverlay(); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             // Sets the screen layout to Landscape | ||||||
|  |             case MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE: | ||||||
|  |                 changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, item); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             // Sets the screen layout to Portrait | ||||||
|  |             case MENU_ACTION_SCREEN_LAYOUT_PORTRAIT: | ||||||
|  |                 changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, item); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             // Sets the screen layout to Single | ||||||
|  |             case MENU_ACTION_SCREEN_LAYOUT_SINGLE: | ||||||
|  |                 changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, item); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             // Sets the screen layout to Side by Side | ||||||
|  |             case MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE: | ||||||
|  |                 changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, item); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             // Swap the top and bottom screen locations | ||||||
|  |             case MENU_ACTION_SWAP_SCREENS: { | ||||||
|  |                 final boolean isEnabled = !EmulationMenuSettings.getSwapScreens(); | ||||||
|  |                 EmulationMenuSettings.setSwapScreens(isEnabled); | ||||||
|  |                 item.setChecked(isEnabled); | ||||||
|  | 
 | ||||||
|  |                 NativeLibrary.SwapScreens(isEnabled, getWindowManager().getDefaultDisplay() | ||||||
|  |                         .getRotation()); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Reset overlay placement | ||||||
|  |             case MENU_ACTION_RESET_OVERLAY: | ||||||
|  |                 resetOverlay(); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             // Show or hide overlay | ||||||
|  |             case MENU_ACTION_SHOW_OVERLAY: { | ||||||
|  |                 final boolean isEnabled = !EmulationMenuSettings.getShowOverlay(); | ||||||
|  |                 EmulationMenuSettings.setShowOverlay(isEnabled); | ||||||
|  |                 item.setChecked(isEnabled); | ||||||
|  | 
 | ||||||
|  |                 mEmulationFragment.refreshInputOverlay(); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             case MENU_ACTION_EXIT: | ||||||
|  |                 mEmulationFragment.stopEmulation(); | ||||||
|  |                 finish(); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             case MENU_ACTION_OPEN_SETTINGS: | ||||||
|  |                 SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, ""); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             case MENU_ACTION_LOAD_AMIIBO: | ||||||
|  |                 FileBrowserHelper.openFilePicker(this, REQUEST_SELECT_AMIIBO, | ||||||
|  |                                                  R.string.select_amiibo, | ||||||
|  |                                                  Collections.singletonList("bin"), false); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             case MENU_ACTION_REMOVE_AMIIBO: | ||||||
|  |                 RemoveAmiibo(); | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |             case MENU_ACTION_JOYSTICK_REL_CENTER: | ||||||
|  |                 final boolean isJoystickRelCenterEnabled = !EmulationMenuSettings.getJoystickRelCenter(); | ||||||
|  |                 EmulationMenuSettings.setJoystickRelCenter(isJoystickRelCenterEnabled); | ||||||
|  |                 item.setChecked(isJoystickRelCenterEnabled); | ||||||
|  |                 break; | ||||||
|  |             case MENU_ACTION_DPAD_SLIDE_ENABLE: | ||||||
|  |                 final boolean isDpadSlideEnabled = !EmulationMenuSettings.getDpadSlideEnable(); | ||||||
|  |                 EmulationMenuSettings.setDpadSlideEnable(isDpadSlideEnabled); | ||||||
|  |                 item.setChecked(isDpadSlideEnabled); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void changeScreenOrientation(int layoutOption, MenuItem item) { | ||||||
|  |         item.setChecked(true); | ||||||
|  |         NativeLibrary.NotifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay() | ||||||
|  |                 .getRotation()); | ||||||
|  |         EmulationMenuSettings.setLandscapeScreenLayout(layoutOption); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void editControlsPlacement() { | ||||||
|  |         if (mEmulationFragment.isConfiguringControls()) { | ||||||
|  |             mEmulationFragment.stopConfiguringControls(); | ||||||
|  |         } else { | ||||||
|  |             mEmulationFragment.startConfiguringControls(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Gets button presses | ||||||
|  |     @Override | ||||||
|  |     public boolean dispatchKeyEvent(KeyEvent event) { | ||||||
|  |         int action; | ||||||
|  |         int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode()); | ||||||
|  | 
 | ||||||
|  |         switch (event.getAction()) { | ||||||
|  |             case KeyEvent.ACTION_DOWN: | ||||||
|  |                 // Handling the case where the back button is pressed. | ||||||
|  |                 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { | ||||||
|  |                     onBackPressed(); | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Normal key events. | ||||||
|  |                 action = NativeLibrary.ButtonState.PRESSED; | ||||||
|  |                 break; | ||||||
|  |             case KeyEvent.ACTION_UP: | ||||||
|  |                 action = NativeLibrary.ButtonState.RELEASED; | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 return false; | ||||||
|  |         } | ||||||
|  |         InputDevice input = event.getDevice(); | ||||||
|  | 
 | ||||||
|  |         if (input == null) { | ||||||
|  |             // Controller was disconnected | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onActivityResult(int requestCode, int resultCode, Intent result) { | ||||||
|  |         super.onActivityResult(requestCode, resultCode, result); | ||||||
|  |         switch (requestCode) { | ||||||
|  |             case StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER: | ||||||
|  |                 StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null); | ||||||
|  |                 break; | ||||||
|  |             case REQUEST_SELECT_AMIIBO: | ||||||
|  |                 // If the user picked a file, as opposed to just backing out. | ||||||
|  |                 if (resultCode == MainActivity.RESULT_OK) { | ||||||
|  |                     String[] selectedFiles = FileBrowserHelper.getSelectedFiles(result); | ||||||
|  |                     if (selectedFiles == null) | ||||||
|  |                         return; | ||||||
|  | 
 | ||||||
|  |                     onAmiiboSelected(selectedFiles[0]); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void onAmiiboSelected(String selectedFile) { | ||||||
|  |         File file = new File(selectedFile); | ||||||
|  |         boolean success = false; | ||||||
|  |         try { | ||||||
|  |             byte[] bytes = FileUtil.getBytesFromFile(file); | ||||||
|  |             success = NativeLibrary.LoadAmiibo(bytes); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             e.printStackTrace(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!success) { | ||||||
|  |             new AlertDialog.Builder(this) | ||||||
|  |                     .setTitle(R.string.amiibo_load_error) | ||||||
|  |                     .setMessage(R.string.amiibo_load_error_message) | ||||||
|  |                     .setPositiveButton(android.R.string.ok, null) | ||||||
|  |                     .create() | ||||||
|  |                     .show(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void RemoveAmiibo() { | ||||||
|  |         NativeLibrary.RemoveAmiibo(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void toggleControls() { | ||||||
|  |         final SharedPreferences.Editor editor = mPreferences.edit(); | ||||||
|  |         boolean[] enabledButtons = new boolean[14]; | ||||||
|  |         AlertDialog.Builder builder = new AlertDialog.Builder(this); | ||||||
|  |         builder.setTitle(R.string.emulation_toggle_controls); | ||||||
|  | 
 | ||||||
|  |         for (int i = 0; i < enabledButtons.length; i++) { | ||||||
|  |             // Buttons that are disabled by default | ||||||
|  |             boolean defaultValue = true; | ||||||
|  |             switch (i) { | ||||||
|  |                 case 6: // ZL | ||||||
|  |                 case 7: // ZR | ||||||
|  |                 case 12: // C-stick | ||||||
|  |                     defaultValue = false; | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             enabledButtons[i] = mPreferences.getBoolean("buttonToggle" + i, defaultValue); | ||||||
|  |         } | ||||||
|  |         builder.setMultiChoiceItems(R.array.n3dsButtons, enabledButtons, | ||||||
|  |                 (dialog, indexSelected, isChecked) -> editor | ||||||
|  |                         .putBoolean("buttonToggle" + indexSelected, isChecked)); | ||||||
|  |         builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> | ||||||
|  |         { | ||||||
|  |             editor.apply(); | ||||||
|  | 
 | ||||||
|  |             mEmulationFragment.refreshInputOverlay(); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         AlertDialog alertDialog = builder.create(); | ||||||
|  |         alertDialog.show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void adjustScale() { | ||||||
|  |         LayoutInflater inflater = LayoutInflater.from(this); | ||||||
|  |         View view = inflater.inflate(R.layout.dialog_seekbar, null); | ||||||
|  | 
 | ||||||
|  |         final SeekBar seekbar = view.findViewById(R.id.seekbar); | ||||||
|  |         final TextView value = view.findViewById(R.id.text_value); | ||||||
|  |         final TextView units = view.findViewById(R.id.text_units); | ||||||
|  | 
 | ||||||
|  |         seekbar.setMax(150); | ||||||
|  |         seekbar.setProgress(mPreferences.getInt("controlScale", 50)); | ||||||
|  |         seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { | ||||||
|  |             public void onStartTrackingTouch(SeekBar seekBar) { | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { | ||||||
|  |                 value.setText(String.valueOf(progress + 50)); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             public void onStopTrackingTouch(SeekBar seekBar) { | ||||||
|  |                 setControlScale(seekbar.getProgress()); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         value.setText(String.valueOf(seekbar.getProgress() + 50)); | ||||||
|  |         units.setText("%"); | ||||||
|  | 
 | ||||||
|  |         AlertDialog.Builder builder = new AlertDialog.Builder(this); | ||||||
|  |         builder.setTitle(R.string.emulation_control_scale); | ||||||
|  |         builder.setView(view); | ||||||
|  |         final int previousProgress = seekbar.getProgress(); | ||||||
|  |         builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> { | ||||||
|  |             setControlScale(previousProgress); | ||||||
|  |         }); | ||||||
|  |         builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> | ||||||
|  |         { | ||||||
|  |             setControlScale(seekbar.getProgress()); | ||||||
|  |         }); | ||||||
|  |         builder.setNeutralButton(R.string.slider_default, (dialogInterface, i) -> { | ||||||
|  |             setControlScale(50); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         AlertDialog alertDialog = builder.create(); | ||||||
|  |         alertDialog.show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setControlScale(int scale) { | ||||||
|  |         SharedPreferences.Editor editor = mPreferences.edit(); | ||||||
|  |         editor.putInt("controlScale", scale); | ||||||
|  |         editor.apply(); | ||||||
|  |         mEmulationFragment.refreshInputOverlay(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void resetOverlay() { | ||||||
|  |         new AlertDialog.Builder(this) | ||||||
|  |                 .setTitle(getString(R.string.emulation_touch_overlay_reset)) | ||||||
|  |                 .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> mEmulationFragment.resetInputOverlay()) | ||||||
|  |                 .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> { | ||||||
|  |                 }) | ||||||
|  |                 .create() | ||||||
|  |                 .show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean dispatchGenericMotionEvent(MotionEvent event) { | ||||||
|  |         if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)) { | ||||||
|  |             return super.dispatchGenericMotionEvent(event); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Don't attempt to do anything if we are disconnecting a device. | ||||||
|  |         if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         InputDevice input = event.getDevice(); | ||||||
|  |         List<InputDevice.MotionRange> motions = input.getMotionRanges(); | ||||||
|  | 
 | ||||||
|  |         float[] axisValuesCirclePad = {0.0f, 0.0f}; | ||||||
|  |         float[] axisValuesCStick = {0.0f, 0.0f}; | ||||||
|  |         float[] axisValuesDPad = {0.0f, 0.0f}; | ||||||
|  |         boolean isTriggerPressedLMapped = false; | ||||||
|  |         boolean isTriggerPressedRMapped = false; | ||||||
|  |         boolean isTriggerPressedZLMapped = false; | ||||||
|  |         boolean isTriggerPressedZRMapped = false; | ||||||
|  |         boolean isTriggerPressedL = false; | ||||||
|  |         boolean isTriggerPressedR = false; | ||||||
|  |         boolean isTriggerPressedZL = false; | ||||||
|  |         boolean isTriggerPressedZR = false; | ||||||
|  | 
 | ||||||
|  |         for (InputDevice.MotionRange range : motions) { | ||||||
|  |             int axis = range.getAxis(); | ||||||
|  |             float origValue = event.getAxisValue(axis); | ||||||
|  |             float value = mControllerMappingHelper.scaleAxis(input, axis, origValue); | ||||||
|  |             int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1); | ||||||
|  |             int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1); | ||||||
|  | 
 | ||||||
|  |             if (nextMapping == -1 || guestOrientation == -1) { | ||||||
|  |                 // Axis is unmapped | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if ((value > 0.f && value < 0.1f) || (value < 0.f && value > -0.1f)) { | ||||||
|  |                 // Skip joystick wobble | ||||||
|  |                 value = 0.f; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (nextMapping == NativeLibrary.ButtonType.STICK_LEFT) { | ||||||
|  |                 axisValuesCirclePad[guestOrientation] = value; | ||||||
|  |             } else if (nextMapping == NativeLibrary.ButtonType.STICK_C) { | ||||||
|  |                 axisValuesCStick[guestOrientation] = value; | ||||||
|  |             } else if (nextMapping == NativeLibrary.ButtonType.DPAD) { | ||||||
|  |                 axisValuesDPad[guestOrientation] = value; | ||||||
|  |             } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_L) { | ||||||
|  |                 isTriggerPressedLMapped = true; | ||||||
|  |                 isTriggerPressedL = value != 0.f; | ||||||
|  |             } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_R) { | ||||||
|  |                 isTriggerPressedRMapped = true; | ||||||
|  |                 isTriggerPressedR = value != 0.f; | ||||||
|  |             } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZL) { | ||||||
|  |                 isTriggerPressedZLMapped = true; | ||||||
|  |                 isTriggerPressedZL = value != 0.f; | ||||||
|  |             } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZR) { | ||||||
|  |                 isTriggerPressedZRMapped = true; | ||||||
|  |                 isTriggerPressedZR = value != 0.f; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Circle-Pad and C-Stick status | ||||||
|  |         NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]); | ||||||
|  |         NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]); | ||||||
|  | 
 | ||||||
|  |         // Triggers L/R and ZL/ZR | ||||||
|  |         if (isTriggerPressedLMapped) { | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); | ||||||
|  |         } | ||||||
|  |         if (isTriggerPressedRMapped) { | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); | ||||||
|  |         } | ||||||
|  |         if (isTriggerPressedZLMapped) { | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); | ||||||
|  |         } | ||||||
|  |         if (isTriggerPressedZRMapped) { | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Work-around to allow D-pad axis to be bound to emulated buttons | ||||||
|  |         if (axisValuesDPad[0] == 0.f) { | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED); | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED); | ||||||
|  |         } | ||||||
|  |         if (axisValuesDPad[0] < 0.f) { | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED); | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED); | ||||||
|  |         } | ||||||
|  |         if (axisValuesDPad[0] > 0.f) { | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED); | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED); | ||||||
|  |         } | ||||||
|  |         if (axisValuesDPad[1] == 0.f) { | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED); | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED); | ||||||
|  |         } | ||||||
|  |         if (axisValuesDPad[1] < 0.f) { | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED); | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED); | ||||||
|  |         } | ||||||
|  |         if (axisValuesDPad[1] > 0.f) { | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED); | ||||||
|  |             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isActivityRecreated() { | ||||||
|  |         return activityRecreated; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Retention(SOURCE) | ||||||
|  |     @IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE, | ||||||
|  |             MENU_ACTION_EXIT, MENU_ACTION_SHOW_FPS, MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE, | ||||||
|  |             MENU_ACTION_SCREEN_LAYOUT_PORTRAIT, MENU_ACTION_SCREEN_LAYOUT_SINGLE, MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE, | ||||||
|  |             MENU_ACTION_SWAP_SCREENS, MENU_ACTION_RESET_OVERLAY, MENU_ACTION_SHOW_OVERLAY, MENU_ACTION_OPEN_SETTINGS}) | ||||||
|  |     public @interface MenuAction { | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,247 @@ | ||||||
|  | package org.citra.citra_emu.adapters; | ||||||
|  | 
 | ||||||
|  | import android.database.Cursor; | ||||||
|  | import android.database.DataSetObserver; | ||||||
|  | import android.graphics.Rect; | ||||||
|  | import android.graphics.drawable.Drawable; | ||||||
|  | import android.os.Build; | ||||||
|  | import android.os.SystemClock; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.RequiresApi; | ||||||
|  | import androidx.core.content.ContextCompat; | ||||||
|  | import androidx.fragment.app.FragmentActivity; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.activities.EmulationActivity; | ||||||
|  | import org.citra.citra_emu.model.GameDatabase; | ||||||
|  | import org.citra.citra_emu.ui.DividerItemDecoration; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | import org.citra.citra_emu.utils.PicassoUtils; | ||||||
|  | import org.citra.citra_emu.viewholders.GameViewHolder; | ||||||
|  | 
 | ||||||
|  | import java.nio.file.Path; | ||||||
|  | import java.nio.file.Paths; | ||||||
|  | import java.util.stream.Stream; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * This adapter gets its information from a database Cursor. This fact, paired with the usage of | ||||||
|  |  * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) | ||||||
|  |  * large dataset. | ||||||
|  |  */ | ||||||
|  | public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements | ||||||
|  |         View.OnClickListener { | ||||||
|  |     private Cursor mCursor; | ||||||
|  |     private GameDataSetObserver mObserver; | ||||||
|  | 
 | ||||||
|  |     private boolean mDatasetValid; | ||||||
|  |     private long mLastClickTime = 0; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will | ||||||
|  |      * display no data until a Cursor is supplied by a CursorLoader. | ||||||
|  |      */ | ||||||
|  |     public GameAdapter() { | ||||||
|  |         mDatasetValid = false; | ||||||
|  |         mObserver = new GameDataSetObserver(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called by the LayoutManager when it is necessary to create a new view. | ||||||
|  |      * | ||||||
|  |      * @param parent   The RecyclerView (I think?) the created view will be thrown into. | ||||||
|  |      * @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView. | ||||||
|  |      * @return The created ViewHolder with references to all the child view's members. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { | ||||||
|  |         // Create a new view. | ||||||
|  |         View gameCard = LayoutInflater.from(parent.getContext()) | ||||||
|  |                 .inflate(R.layout.card_game, parent, false); | ||||||
|  | 
 | ||||||
|  |         gameCard.setOnClickListener(this); | ||||||
|  | 
 | ||||||
|  |         // Use that view to create a ViewHolder. | ||||||
|  |         return new GameViewHolder(gameCard); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called by the LayoutManager when a new view is not necessary because we can recycle | ||||||
|  |      * an existing one (for example, if a view just scrolled onto the screen from the bottom, we | ||||||
|  |      * can use the view that just scrolled off the top instead of inflating a new one.) | ||||||
|  |      * | ||||||
|  |      * @param holder   A ViewHolder representing the view we're recycling. | ||||||
|  |      * @param position The position of the 'new' view in the dataset. | ||||||
|  |      */ | ||||||
|  |     @RequiresApi(api = Build.VERSION_CODES.O) | ||||||
|  |     @Override | ||||||
|  |     public void onBindViewHolder(@NonNull GameViewHolder holder, int position) { | ||||||
|  |         if (mDatasetValid) { | ||||||
|  |             if (mCursor.moveToPosition(position)) { | ||||||
|  |                 PicassoUtils.loadGameIcon(holder.imageIcon, | ||||||
|  |                         mCursor.getString(GameDatabase.GAME_COLUMN_PATH)); | ||||||
|  | 
 | ||||||
|  |                 holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " ")); | ||||||
|  |                 holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); | ||||||
|  | 
 | ||||||
|  |                 final Path gamePath = Paths.get(mCursor.getString(GameDatabase.GAME_COLUMN_PATH)); | ||||||
|  |                 holder.textFileName.setText(gamePath.getFileName().toString()); | ||||||
|  | 
 | ||||||
|  |                 // TODO These shouldn't be necessary once the move to a DB-based model is complete. | ||||||
|  |                 holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID); | ||||||
|  |                 holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH); | ||||||
|  |                 holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE); | ||||||
|  |                 holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION); | ||||||
|  |                 holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS); | ||||||
|  |                 holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY); | ||||||
|  | 
 | ||||||
|  |                 final int backgroundColorId = isValidGame(holder.path) ? R.color.card_view_background : R.color.card_view_disabled; | ||||||
|  |                 View itemView = holder.getItemView(); | ||||||
|  |                 itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), backgroundColorId)); | ||||||
|  |             } else { | ||||||
|  |                 Log.error("[GameAdapter] Can't bind view; Cursor is not valid."); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             Log.error("[GameAdapter] Can't bind view; dataset is not valid."); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called by the LayoutManager to find out how much data we have. | ||||||
|  |      * | ||||||
|  |      * @return Size of the dataset. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public int getItemCount() { | ||||||
|  |         if (mDatasetValid && mCursor != null) { | ||||||
|  |             return mCursor.getCount(); | ||||||
|  |         } | ||||||
|  |         Log.error("[GameAdapter] Dataset is not valid."); | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Return the contents of the _id column for a given row. | ||||||
|  |      * | ||||||
|  |      * @param position The row for which Android wants an ID. | ||||||
|  |      * @return A valid ID from the database, or 0 if not available. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public long getItemId(int position) { | ||||||
|  |         if (mDatasetValid && mCursor != null) { | ||||||
|  |             if (mCursor.moveToPosition(position)) { | ||||||
|  |                 return mCursor.getLong(GameDatabase.COLUMN_DB_ID); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Log.error("[GameAdapter] Dataset is not valid."); | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Tell Android whether or not each item in the dataset has a stable identifier. | ||||||
|  |      * Which it does, because it's a database, so always tell Android 'true'. | ||||||
|  |      * | ||||||
|  |      * @param hasStableIds ignored. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void setHasStableIds(boolean hasStableIds) { | ||||||
|  |         super.setHasStableIds(true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * When a load is finished, call this to replace the existing data with the newly-loaded | ||||||
|  |      * data. | ||||||
|  |      * | ||||||
|  |      * @param cursor The newly-loaded Cursor. | ||||||
|  |      */ | ||||||
|  |     public void swapCursor(Cursor cursor) { | ||||||
|  |         // Sanity check. | ||||||
|  |         if (cursor == mCursor) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Before getting rid of the old cursor, disassociate it from the Observer. | ||||||
|  |         final Cursor oldCursor = mCursor; | ||||||
|  |         if (oldCursor != null && mObserver != null) { | ||||||
|  |             oldCursor.unregisterDataSetObserver(mObserver); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         mCursor = cursor; | ||||||
|  |         if (mCursor != null) { | ||||||
|  |             // Attempt to associate the new Cursor with the Observer. | ||||||
|  |             if (mObserver != null) { | ||||||
|  |                 mCursor.registerDataSetObserver(mObserver); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             mDatasetValid = true; | ||||||
|  |         } else { | ||||||
|  |             mDatasetValid = false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         notifyDataSetChanged(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Launches the game that was clicked on. | ||||||
|  |      * | ||||||
|  |      * @param view The card representing the game the user wants to play. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onClick(View view) { | ||||||
|  |         // Double-click prevention, using threshold of 1000 ms | ||||||
|  |         if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         mLastClickTime = SystemClock.elapsedRealtime(); | ||||||
|  | 
 | ||||||
|  |         GameViewHolder holder = (GameViewHolder) view.getTag(); | ||||||
|  | 
 | ||||||
|  |         EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static class SpacesItemDecoration extends DividerItemDecoration { | ||||||
|  |         private int space; | ||||||
|  | 
 | ||||||
|  |         public SpacesItemDecoration(Drawable divider, int space) { | ||||||
|  |             super(divider); | ||||||
|  |             this.space = space; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void getItemOffsets(Rect outRect, @NonNull View view, @NonNull RecyclerView parent, | ||||||
|  |                                    @NonNull RecyclerView.State state) { | ||||||
|  |             outRect.left = 0; | ||||||
|  |             outRect.right = 0; | ||||||
|  |             outRect.bottom = space; | ||||||
|  |             outRect.top = 0; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean isValidGame(String path) { | ||||||
|  |         return Stream.of( | ||||||
|  |                 ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private final class GameDataSetObserver extends DataSetObserver { | ||||||
|  |         @Override | ||||||
|  |         public void onChanged() { | ||||||
|  |             super.onChanged(); | ||||||
|  | 
 | ||||||
|  |             mDatasetValid = true; | ||||||
|  |             notifyDataSetChanged(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public void onInvalidated() { | ||||||
|  |             super.onInvalidated(); | ||||||
|  | 
 | ||||||
|  |             mDatasetValid = false; | ||||||
|  |             notifyDataSetChanged(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,124 @@ | ||||||
|  | // Copyright 2020 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.applets; | ||||||
|  | 
 | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.app.Dialog; | ||||||
|  | import android.content.DialogInterface; | ||||||
|  | import android.os.Bundle; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.activities.EmulationActivity; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.Objects; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | import androidx.fragment.app.DialogFragment; | ||||||
|  | 
 | ||||||
|  | public final class MiiSelector { | ||||||
|  |     public static class MiiSelectorConfig implements java.io.Serializable { | ||||||
|  |         public boolean enable_cancel_button; | ||||||
|  |         public String title; | ||||||
|  |         public long initially_selected_mii_index; | ||||||
|  |         // List of Miis to display | ||||||
|  |         public String[] mii_names; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static class MiiSelectorData { | ||||||
|  |         public long return_code; | ||||||
|  |         public int index; | ||||||
|  | 
 | ||||||
|  |         private MiiSelectorData(long return_code, int index) { | ||||||
|  |             this.return_code = return_code; | ||||||
|  |             this.index = index; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static class MiiSelectorDialogFragment extends DialogFragment { | ||||||
|  |         static MiiSelectorDialogFragment newInstance(MiiSelectorConfig config) { | ||||||
|  |             MiiSelectorDialogFragment frag = new MiiSelectorDialogFragment(); | ||||||
|  |             Bundle args = new Bundle(); | ||||||
|  |             args.putSerializable("config", config); | ||||||
|  |             frag.setArguments(args); | ||||||
|  |             return frag; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         @Override | ||||||
|  |         public Dialog onCreateDialog(Bundle savedInstanceState) { | ||||||
|  |             final Activity emulationActivity = Objects.requireNonNull(getActivity()); | ||||||
|  | 
 | ||||||
|  |             MiiSelectorConfig config = | ||||||
|  |                     Objects.requireNonNull((MiiSelectorConfig) Objects.requireNonNull(getArguments()) | ||||||
|  |                             .getSerializable("config")); | ||||||
|  | 
 | ||||||
|  |             // Note: we intentionally leave out the Standard Mii in the native code so that | ||||||
|  |             // the string can get translated | ||||||
|  |             ArrayList<String> list = new ArrayList<>(); | ||||||
|  |             list.add(emulationActivity.getString(R.string.standard_mii)); | ||||||
|  |             list.addAll(Arrays.asList(config.mii_names)); | ||||||
|  | 
 | ||||||
|  |             final int initialIndex = config.initially_selected_mii_index < list.size() | ||||||
|  |                     ? (int) config.initially_selected_mii_index | ||||||
|  |                     : 0; | ||||||
|  |             data.index = initialIndex; | ||||||
|  |             AlertDialog.Builder builder = | ||||||
|  |                     new AlertDialog.Builder(emulationActivity) | ||||||
|  |                             .setTitle(config.title.isEmpty() | ||||||
|  |                                     ? emulationActivity.getString(R.string.mii_selector) | ||||||
|  |                                     : config.title) | ||||||
|  |                             .setSingleChoiceItems(list.toArray(new String[]{}), initialIndex, | ||||||
|  |                                     (dialog, which) -> { | ||||||
|  |                                         data.index = which; | ||||||
|  |                                     }) | ||||||
|  |                             .setPositiveButton(android.R.string.ok, (dialog, which) -> { | ||||||
|  |                                 data.return_code = 0; | ||||||
|  |                                 synchronized (finishLock) { | ||||||
|  |                                     finishLock.notifyAll(); | ||||||
|  |                                 } | ||||||
|  |                             }); | ||||||
|  |             if (config.enable_cancel_button) { | ||||||
|  |                 builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> { | ||||||
|  |                     data.return_code = 1; | ||||||
|  |                     synchronized (finishLock) { | ||||||
|  |                         finishLock.notifyAll(); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |             setCancelable(false); | ||||||
|  |             return builder.create(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static MiiSelectorData data; | ||||||
|  |     private static final Object finishLock = new Object(); | ||||||
|  | 
 | ||||||
|  |     private static void ExecuteImpl(MiiSelectorConfig config) { | ||||||
|  |         final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); | ||||||
|  | 
 | ||||||
|  |         data = new MiiSelectorData(0, 0); | ||||||
|  | 
 | ||||||
|  |         MiiSelectorDialogFragment fragment = MiiSelectorDialogFragment.newInstance(config); | ||||||
|  |         fragment.show(emulationActivity.getSupportFragmentManager(), "mii_selector"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static MiiSelectorData Execute(MiiSelectorConfig config) { | ||||||
|  |         NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); | ||||||
|  | 
 | ||||||
|  |         synchronized (finishLock) { | ||||||
|  |             try { | ||||||
|  |                 finishLock.wait(); | ||||||
|  |             } catch (Exception ignored) { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,264 @@ | ||||||
|  | // Copyright 2020 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.applets; | ||||||
|  | 
 | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.app.Dialog; | ||||||
|  | import android.content.DialogInterface; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.text.InputFilter; | ||||||
|  | import android.text.Spanned; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.widget.EditText; | ||||||
|  | import android.widget.FrameLayout; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | import androidx.fragment.app.DialogFragment; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.activities.EmulationActivity; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | 
 | ||||||
|  | import java.util.Objects; | ||||||
|  | 
 | ||||||
|  | public final class SoftwareKeyboard { | ||||||
|  |     /// Corresponds to Frontend::ButtonConfig | ||||||
|  |     private interface ButtonConfig { | ||||||
|  |         int Single = 0; /// Ok button | ||||||
|  |         int Dual = 1;   /// Cancel | Ok buttons | ||||||
|  |         int Triple = 2; /// Cancel | I Forgot | Ok buttons | ||||||
|  |         int None = 3;   /// No button (returned by swkbdInputText in special cases) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Corresponds to Frontend::ValidationError | ||||||
|  |     public enum ValidationError { | ||||||
|  |         None, | ||||||
|  |         // Button Selection | ||||||
|  |         ButtonOutOfRange, | ||||||
|  |         // Configured Filters | ||||||
|  |         MaxDigitsExceeded, | ||||||
|  |         AtSignNotAllowed, | ||||||
|  |         PercentNotAllowed, | ||||||
|  |         BackslashNotAllowed, | ||||||
|  |         ProfanityNotAllowed, | ||||||
|  |         CallbackFailed, | ||||||
|  |         // Allowed Input Type | ||||||
|  |         FixedLengthRequired, | ||||||
|  |         MaxLengthExceeded, | ||||||
|  |         BlankInputNotAllowed, | ||||||
|  |         EmptyInputNotAllowed, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static class KeyboardConfig implements java.io.Serializable { | ||||||
|  |         public int button_config; | ||||||
|  |         public int max_text_length; | ||||||
|  |         public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input | ||||||
|  |         public String hint_text;       /// Displayed in the field as a hint before | ||||||
|  |         @Nullable | ||||||
|  |         public String[] button_text; /// Contains the button text that the caller provides | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Corresponds to Frontend::KeyboardData | ||||||
|  |     public static class KeyboardData { | ||||||
|  |         public int button; | ||||||
|  |         public String text; | ||||||
|  | 
 | ||||||
|  |         private KeyboardData(int button, String text) { | ||||||
|  |             this.button = button; | ||||||
|  |             this.text = text; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static class Filter implements InputFilter { | ||||||
|  |         @Override | ||||||
|  |         public CharSequence filter(CharSequence source, int start, int end, Spanned dest, | ||||||
|  |                                    int dstart, int dend) { | ||||||
|  |             String text = new StringBuilder(dest) | ||||||
|  |                     .replace(dstart, dend, source.subSequence(start, end).toString()) | ||||||
|  |                     .toString(); | ||||||
|  |             if (ValidateFilters(text) == ValidationError.None) { | ||||||
|  |                 return null; // Accept replacement | ||||||
|  |             } | ||||||
|  |             return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static class KeyboardDialogFragment extends DialogFragment { | ||||||
|  |         static KeyboardDialogFragment newInstance(KeyboardConfig config) { | ||||||
|  |             KeyboardDialogFragment frag = new KeyboardDialogFragment(); | ||||||
|  |             Bundle args = new Bundle(); | ||||||
|  |             args.putSerializable("config", config); | ||||||
|  |             frag.setArguments(args); | ||||||
|  |             return frag; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         @Override | ||||||
|  |         public Dialog onCreateDialog(Bundle savedInstanceState) { | ||||||
|  |             final Activity emulationActivity = getActivity(); | ||||||
|  |             assert emulationActivity != null; | ||||||
|  | 
 | ||||||
|  |             FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( | ||||||
|  |                     ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); | ||||||
|  |             params.leftMargin = params.rightMargin = | ||||||
|  |                     CitraApplication.getAppContext().getResources().getDimensionPixelSize( | ||||||
|  |                             R.dimen.dialog_margin); | ||||||
|  | 
 | ||||||
|  |             KeyboardConfig config = Objects.requireNonNull( | ||||||
|  |                     (KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config")); | ||||||
|  | 
 | ||||||
|  |             // Set up the input | ||||||
|  |             EditText editText = new EditText(CitraApplication.getAppContext()); | ||||||
|  |             editText.setHint(config.hint_text); | ||||||
|  |             editText.setSingleLine(!config.multiline_mode); | ||||||
|  |             editText.setLayoutParams(params); | ||||||
|  |             editText.setFilters(new InputFilter[]{ | ||||||
|  |                     new Filter(), new InputFilter.LengthFilter(config.max_text_length)}); | ||||||
|  | 
 | ||||||
|  |             FrameLayout container = new FrameLayout(emulationActivity); | ||||||
|  |             container.addView(editText); | ||||||
|  | 
 | ||||||
|  |             AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) | ||||||
|  |                     .setTitle(R.string.software_keyboard) | ||||||
|  |                     .setView(container); | ||||||
|  |             setCancelable(false); | ||||||
|  | 
 | ||||||
|  |             switch (config.button_config) { | ||||||
|  |                 case ButtonConfig.Triple: { | ||||||
|  |                     final String text = config.button_text[1].isEmpty() | ||||||
|  |                             ? emulationActivity.getString(R.string.i_forgot) | ||||||
|  |                             : config.button_text[1]; | ||||||
|  |                     builder.setNeutralButton(text, null); | ||||||
|  |                 } | ||||||
|  |                 // fallthrough | ||||||
|  |                 case ButtonConfig.Dual: { | ||||||
|  |                     final String text = config.button_text[0].isEmpty() | ||||||
|  |                             ? emulationActivity.getString(android.R.string.cancel) | ||||||
|  |                             : config.button_text[0]; | ||||||
|  |                     builder.setNegativeButton(text, null); | ||||||
|  |                 } | ||||||
|  |                 // fallthrough | ||||||
|  |                 case ButtonConfig.Single: { | ||||||
|  |                     final String text = config.button_text[2].isEmpty() | ||||||
|  |                             ? emulationActivity.getString(android.R.string.ok) | ||||||
|  |                             : config.button_text[2]; | ||||||
|  |                     builder.setPositiveButton(text, null); | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             final AlertDialog dialog = builder.create(); | ||||||
|  |             dialog.create(); | ||||||
|  |             if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) { | ||||||
|  |                 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> { | ||||||
|  |                     data.button = config.button_config; | ||||||
|  |                     data.text = editText.getText().toString(); | ||||||
|  |                     final ValidationError error = ValidateInput(data.text); | ||||||
|  |                     if (error != ValidationError.None) { | ||||||
|  |                         HandleValidationError(config, error); | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     dialog.dismiss(); | ||||||
|  | 
 | ||||||
|  |                     synchronized (finishLock) { | ||||||
|  |                         finishLock.notifyAll(); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |             if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) { | ||||||
|  |                 dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> { | ||||||
|  |                     data.button = 1; | ||||||
|  |                     dialog.dismiss(); | ||||||
|  |                     synchronized (finishLock) { | ||||||
|  |                         finishLock.notifyAll(); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |             if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) { | ||||||
|  |                 dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> { | ||||||
|  |                     data.button = 0; | ||||||
|  |                     dialog.dismiss(); | ||||||
|  |                     synchronized (finishLock) { | ||||||
|  |                         finishLock.notifyAll(); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return dialog; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static KeyboardData data; | ||||||
|  |     private static final Object finishLock = new Object(); | ||||||
|  | 
 | ||||||
|  |     private static void ExecuteImpl(KeyboardConfig config) { | ||||||
|  |         final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); | ||||||
|  | 
 | ||||||
|  |         data = new KeyboardData(0, ""); | ||||||
|  | 
 | ||||||
|  |         KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config); | ||||||
|  |         fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void HandleValidationError(KeyboardConfig config, ValidationError error) { | ||||||
|  |         final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); | ||||||
|  |         String message = ""; | ||||||
|  |         switch (error) { | ||||||
|  |             case FixedLengthRequired: | ||||||
|  |                 message = | ||||||
|  |                         emulationActivity.getString(R.string.fixed_length_required, config.max_text_length); | ||||||
|  |                 break; | ||||||
|  |             case MaxLengthExceeded: | ||||||
|  |                 message = | ||||||
|  |                         emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length); | ||||||
|  |                 break; | ||||||
|  |             case BlankInputNotAllowed: | ||||||
|  |                 message = emulationActivity.getString(R.string.blank_input_not_allowed); | ||||||
|  |                 break; | ||||||
|  |             case EmptyInputNotAllowed: | ||||||
|  |                 message = emulationActivity.getString(R.string.empty_input_not_allowed); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         new AlertDialog.Builder(emulationActivity) | ||||||
|  |                 .setTitle(R.string.software_keyboard) | ||||||
|  |                 .setMessage(message) | ||||||
|  |                 .setPositiveButton(android.R.string.ok, null) | ||||||
|  |                 .show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static KeyboardData Execute(KeyboardConfig config) { | ||||||
|  |         if (config.button_config == ButtonConfig.None) { | ||||||
|  |             Log.error("Unexpected button config None"); | ||||||
|  |             return new KeyboardData(0, ""); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); | ||||||
|  | 
 | ||||||
|  |         synchronized (finishLock) { | ||||||
|  |             try { | ||||||
|  |                 finishLock.wait(); | ||||||
|  |             } catch (Exception ignored) { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void ShowError(String error) { | ||||||
|  |         NativeLibrary.displayAlertMsg( | ||||||
|  |                 CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard), | ||||||
|  |                 error, false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static native ValidationError ValidateFilters(String text); | ||||||
|  | 
 | ||||||
|  |     private static native ValidationError ValidateInput(String text); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,65 @@ | ||||||
|  | // Copyright 2020 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.camera; | ||||||
|  | 
 | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | import android.provider.MediaStore; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.activities.EmulationActivity; | ||||||
|  | import org.citra.citra_emu.utils.PicassoUtils; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | // Used in native code. | ||||||
|  | public final class StillImageCameraHelper { | ||||||
|  |     public static final int REQUEST_CAMERA_FILE_PICKER = 1; | ||||||
|  |     private static final Object filePickerLock = new Object(); | ||||||
|  |     private static @Nullable | ||||||
|  |     String filePickerPath; | ||||||
|  | 
 | ||||||
|  |     // Opens file picker for camera. | ||||||
|  |     public static @Nullable | ||||||
|  |     String OpenFilePicker() { | ||||||
|  |         final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); | ||||||
|  | 
 | ||||||
|  |         // At this point, we are assuming that we already have permissions as they are | ||||||
|  |         // needed to launch a game | ||||||
|  |         emulationActivity.runOnUiThread(() -> { | ||||||
|  |             Intent intent = new Intent(Intent.ACTION_PICK); | ||||||
|  |             intent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*"); | ||||||
|  |             emulationActivity.startActivityForResult( | ||||||
|  |                     Intent.createChooser(intent, | ||||||
|  |                             emulationActivity.getString(R.string.camera_select_image)), | ||||||
|  |                     REQUEST_CAMERA_FILE_PICKER); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         synchronized (filePickerLock) { | ||||||
|  |             try { | ||||||
|  |                 filePickerLock.wait(); | ||||||
|  |             } catch (InterruptedException ignored) { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return filePickerPath; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Called from EmulationActivity. | ||||||
|  |     public static void OnFilePickerResult(Intent result) { | ||||||
|  |         filePickerPath = result == null ? null : result.getDataString(); | ||||||
|  | 
 | ||||||
|  |         synchronized (filePickerLock) { | ||||||
|  |             filePickerLock.notifyAll(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Blocking call. Load image from file and crop/resize it to fit in width x height. | ||||||
|  |     @Nullable | ||||||
|  |     public static Bitmap LoadImageFromFile(String uri, int width, int height) { | ||||||
|  |         return PicassoUtils.LoadBitmapFromFile(uri, width, height); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,140 @@ | ||||||
|  | package org.citra.citra_emu.dialogs; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.view.InputDevice; | ||||||
|  | import android.view.KeyEvent; | ||||||
|  | import android.view.MotionEvent; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * {@link AlertDialog} derivative that listens for | ||||||
|  |  * motion events from controllers and joysticks. | ||||||
|  |  */ | ||||||
|  | public final class MotionAlertDialog extends AlertDialog { | ||||||
|  |     // The selected input preference | ||||||
|  |     private final InputBindingSetting setting; | ||||||
|  |     private final ArrayList<Float> mPreviousValues = new ArrayList<>(); | ||||||
|  |     private int mPrevDeviceId = 0; | ||||||
|  |     private boolean mWaitingForEvent = true; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constructor | ||||||
|  |      * | ||||||
|  |      * @param context The current {@link Context}. | ||||||
|  |      * @param setting The Preference to show this dialog for. | ||||||
|  |      */ | ||||||
|  |     public MotionAlertDialog(Context context, InputBindingSetting setting) { | ||||||
|  |         super(context); | ||||||
|  | 
 | ||||||
|  |         this.setting = setting; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean onKeyEvent(int keyCode, KeyEvent event) { | ||||||
|  |         Log.debug("[MotionAlertDialog] Received key event: " + event.getAction()); | ||||||
|  |         switch (event.getAction()) { | ||||||
|  |             case KeyEvent.ACTION_UP: | ||||||
|  |                 setting.onKeyInput(event); | ||||||
|  |                 dismiss(); | ||||||
|  |                 // Even if we ignore the key, we still consume it. Thus return true regardless. | ||||||
|  |                 return true; | ||||||
|  | 
 | ||||||
|  |             default: | ||||||
|  |                 return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) { | ||||||
|  |         return super.onKeyLongPress(keyCode, event); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean dispatchKeyEvent(KeyEvent event) { | ||||||
|  |         // Handle this key if we care about it, otherwise pass it down the framework | ||||||
|  |         return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) { | ||||||
|  |         // Handle this event if we care about it, otherwise pass it down the framework | ||||||
|  |         return onMotionEvent(event) || super.dispatchGenericMotionEvent(event); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean onMotionEvent(MotionEvent event) { | ||||||
|  |         if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) | ||||||
|  |             return false; | ||||||
|  |         if (event.getAction() != MotionEvent.ACTION_MOVE) | ||||||
|  |             return false; | ||||||
|  | 
 | ||||||
|  |         InputDevice input = event.getDevice(); | ||||||
|  | 
 | ||||||
|  |         List<InputDevice.MotionRange> motionRanges = input.getMotionRanges(); | ||||||
|  | 
 | ||||||
|  |         if (input.getId() != mPrevDeviceId) { | ||||||
|  |             mPreviousValues.clear(); | ||||||
|  |         } | ||||||
|  |         mPrevDeviceId = input.getId(); | ||||||
|  |         boolean firstEvent = mPreviousValues.isEmpty(); | ||||||
|  | 
 | ||||||
|  |         int numMovedAxis = 0; | ||||||
|  |         float axisMoveValue = 0.0f; | ||||||
|  |         InputDevice.MotionRange lastMovedRange = null; | ||||||
|  |         char lastMovedDir = '?'; | ||||||
|  |         if (mWaitingForEvent) { | ||||||
|  |             for (int i = 0; i < motionRanges.size(); i++) { | ||||||
|  |                 InputDevice.MotionRange range = motionRanges.get(i); | ||||||
|  |                 int axis = range.getAxis(); | ||||||
|  |                 float origValue = event.getAxisValue(axis); | ||||||
|  |                 float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue); | ||||||
|  |                 if (firstEvent) { | ||||||
|  |                     mPreviousValues.add(value); | ||||||
|  |                 } else { | ||||||
|  |                     float previousValue = mPreviousValues.get(i); | ||||||
|  | 
 | ||||||
|  |                     // Only handle the axes that are not neutral (more than 0.5) | ||||||
|  |                     // but ignore any axis that has a constant value (e.g. always 1) | ||||||
|  |                     if (Math.abs(value) > 0.5f && value != previousValue) { | ||||||
|  |                         // It is common to have multiple axes with the same physical input. For example, | ||||||
|  |                         // shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE. | ||||||
|  |                         // To handle this, we ignore an axis motion that's the exact same as a motion | ||||||
|  |                         // we already saw. This way, we ignore axes with two names, but catch the case | ||||||
|  |                         // where a joystick is moved in two directions. | ||||||
|  |                         // ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html | ||||||
|  |                         if (value != axisMoveValue) { | ||||||
|  |                             axisMoveValue = value; | ||||||
|  |                             numMovedAxis++; | ||||||
|  |                             lastMovedRange = range; | ||||||
|  |                             lastMovedDir = value < 0.0f ? '-' : '+'; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     // Special case for d-pads (axis value jumps between 0 and 1 without any values | ||||||
|  |                     // in between). Without this, the user would need to press the d-pad twice | ||||||
|  |                     // due to the first press being caught by the "if (firstEvent)" case further up. | ||||||
|  |                     else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) { | ||||||
|  |                         numMovedAxis++; | ||||||
|  |                         lastMovedRange = range; | ||||||
|  |                         lastMovedDir = previousValue < 0.0f ? '-' : '+'; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 mPreviousValues.set(i, value); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // If only one axis moved, that's the winner. | ||||||
|  |             if (numMovedAxis == 1) { | ||||||
|  |                 mWaitingForEvent = false; | ||||||
|  |                 setting.onMotionInput(input, lastMovedRange, lastMovedDir); | ||||||
|  |                 dismiss(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,139 @@ | ||||||
|  | // Copyright 2021 Citra Emulator Project | ||||||
|  | // Licensed under GPLv2 or any later version | ||||||
|  | // Refer to the license.txt file included. | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.disk_shader_cache; | ||||||
|  | 
 | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.app.Dialog; | ||||||
|  | import android.content.DialogInterface; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.os.Handler; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.ProgressBar; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | import androidx.fragment.app.DialogFragment; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.activities.EmulationActivity; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | 
 | ||||||
|  | import java.util.Objects; | ||||||
|  | 
 | ||||||
|  | public class DiskShaderCacheProgress { | ||||||
|  | 
 | ||||||
|  |     // Equivalent to VideoCore::LoadCallbackStage | ||||||
|  |     public enum LoadCallbackStage { | ||||||
|  |         Prepare, | ||||||
|  |         Decompile, | ||||||
|  |         Build, | ||||||
|  |         Complete, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static final Object finishLock = new Object(); | ||||||
|  |     private static ProgressDialogFragment fragment; | ||||||
|  | 
 | ||||||
|  |     public static class ProgressDialogFragment extends DialogFragment { | ||||||
|  |         ProgressBar progressBar; | ||||||
|  |         TextView progressText; | ||||||
|  |         AlertDialog dialog; | ||||||
|  | 
 | ||||||
|  |         static ProgressDialogFragment newInstance(String title, String message) { | ||||||
|  |             ProgressDialogFragment frag = new ProgressDialogFragment(); | ||||||
|  |             Bundle args = new Bundle(); | ||||||
|  |             args.putString("title", title); | ||||||
|  |             args.putString("message", message); | ||||||
|  |             frag.setArguments(args); | ||||||
|  |             return frag; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @NonNull | ||||||
|  |         @Override | ||||||
|  |         public Dialog onCreateDialog(Bundle savedInstanceState) { | ||||||
|  |             final Activity emulationActivity = Objects.requireNonNull(getActivity()); | ||||||
|  | 
 | ||||||
|  |             final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title")); | ||||||
|  |             final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message")); | ||||||
|  | 
 | ||||||
|  |             LayoutInflater inflater = LayoutInflater.from(emulationActivity); | ||||||
|  |             View view = inflater.inflate(R.layout.dialog_progress_bar, null); | ||||||
|  | 
 | ||||||
|  |             progressBar = view.findViewById(R.id.progress_bar); | ||||||
|  |             progressText = view.findViewById(R.id.progress_text); | ||||||
|  |             progressText.setText(""); | ||||||
|  | 
 | ||||||
|  |             setCancelable(false); | ||||||
|  |             setRetainInstance(true); | ||||||
|  | 
 | ||||||
|  |             AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity); | ||||||
|  |             builder.setTitle(title); | ||||||
|  |             builder.setMessage(message); | ||||||
|  |             builder.setView(view); | ||||||
|  |             builder.setNegativeButton(android.R.string.cancel, null); | ||||||
|  | 
 | ||||||
|  |             dialog = builder.create(); | ||||||
|  |             dialog.create(); | ||||||
|  | 
 | ||||||
|  |             dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v) -> emulationActivity.onBackPressed()); | ||||||
|  | 
 | ||||||
|  |             synchronized (finishLock) { | ||||||
|  |                 finishLock.notifyAll(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return dialog; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private void onUpdateProgress(String msg, int progress, int max) { | ||||||
|  |             Objects.requireNonNull(getActivity()).runOnUiThread(() -> { | ||||||
|  |                 progressBar.setProgress(progress); | ||||||
|  |                 progressBar.setMax(max); | ||||||
|  |                 progressText.setText(String.format("%d/%d", progress, max)); | ||||||
|  |                 dialog.setMessage(msg); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void prepareDialog() { | ||||||
|  |         NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> { | ||||||
|  |             final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); | ||||||
|  |             fragment = ProgressDialogFragment.newInstance(emulationActivity.getString(R.string.loading), emulationActivity.getString(R.string.preparing_shaders)); | ||||||
|  |             fragment.show(emulationActivity.getSupportFragmentManager(), "diskShaders"); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         synchronized (finishLock) { | ||||||
|  |             try { | ||||||
|  |                 finishLock.wait(); | ||||||
|  |             } catch (Exception ignored) { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void loadProgress(LoadCallbackStage stage, int progress, int max) { | ||||||
|  |         final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); | ||||||
|  |         if (emulationActivity == null) { | ||||||
|  |             Log.error("[DiskShaderCacheProgress] EmulationActivity not present"); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         switch (stage) { | ||||||
|  |             case Prepare: | ||||||
|  |                 prepareDialog(); | ||||||
|  |                 break; | ||||||
|  |             case Decompile: | ||||||
|  |                 fragment.onUpdateProgress(emulationActivity.getString(R.string.preparing_shaders), progress, max); | ||||||
|  |                 break; | ||||||
|  |             case Build: | ||||||
|  |                 fragment.onUpdateProgress(emulationActivity.getString(R.string.building_shaders), progress, max); | ||||||
|  |                 break; | ||||||
|  |             case Complete: | ||||||
|  |                 // Workaround for when dialog is dismissed when the app is in the background | ||||||
|  |                 fragment.dismissAllowingStateLoss(); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model; | ||||||
|  | 
 | ||||||
|  | public final class BooleanSetting extends Setting { | ||||||
|  |     private boolean mValue; | ||||||
|  | 
 | ||||||
|  |     public BooleanSetting(String key, String section, boolean value) { | ||||||
|  |         super(key, section); | ||||||
|  |         mValue = value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean getValue() { | ||||||
|  |         return mValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setValue(boolean value) { | ||||||
|  |         mValue = value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String getValueAsString() { | ||||||
|  |         return mValue ? "True" : "False"; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model; | ||||||
|  | 
 | ||||||
|  | public final class FloatSetting extends Setting { | ||||||
|  |     private float mValue; | ||||||
|  | 
 | ||||||
|  |     public FloatSetting(String key, String section, float value) { | ||||||
|  |         super(key, section); | ||||||
|  |         mValue = value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public float getValue() { | ||||||
|  |         return mValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setValue(float value) { | ||||||
|  |         mValue = value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String getValueAsString() { | ||||||
|  |         return Float.toString(mValue); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model; | ||||||
|  | 
 | ||||||
|  | public final class IntSetting extends Setting { | ||||||
|  |     private int mValue; | ||||||
|  | 
 | ||||||
|  |     public IntSetting(String key, String section, int value) { | ||||||
|  |         super(key, section); | ||||||
|  |         mValue = value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getValue() { | ||||||
|  |         return mValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setValue(int value) { | ||||||
|  |         mValue = value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String getValueAsString() { | ||||||
|  |         return Integer.toString(mValue); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,42 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Abstraction for a setting item as read from / written to Citra's configuration ini files. | ||||||
|  |  * These files generally consist of a key/value pair, though the type of value is ambiguous and | ||||||
|  |  * must be inferred at read-time. The type of value determines which child of this class is used | ||||||
|  |  * to represent the Setting. | ||||||
|  |  */ | ||||||
|  | public abstract class Setting { | ||||||
|  |     private String mKey; | ||||||
|  |     private String mSection; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Base constructor. | ||||||
|  |      * | ||||||
|  |      * @param key     Everything to the left of the = in a line from the ini file. | ||||||
|  |      * @param section The corresponding recent section header; e.g. [Core] or [Enhancements] without the brackets. | ||||||
|  |      */ | ||||||
|  |     public Setting(String key, String section) { | ||||||
|  |         mKey = key; | ||||||
|  |         mSection = section; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return The identifier used to write this setting to the ini file. | ||||||
|  |      */ | ||||||
|  |     public String getKey() { | ||||||
|  |         return mKey; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return The name of the header under which this Setting should be written in the ini file. | ||||||
|  |      */ | ||||||
|  |     public String getSection() { | ||||||
|  |         return mSection; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return A representation of this Setting's backing value converted to a String (e.g. for serialization). | ||||||
|  |      */ | ||||||
|  |     public abstract String getValueAsString(); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,55 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model; | ||||||
|  | 
 | ||||||
|  | import java.util.HashMap; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A semantically-related group of Settings objects. These Settings are | ||||||
|  |  * internally stored as a HashMap. | ||||||
|  |  */ | ||||||
|  | public final class SettingSection { | ||||||
|  |     private String mName; | ||||||
|  | 
 | ||||||
|  |     private HashMap<String, Setting> mSettings = new HashMap<>(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a new SettingSection with no Settings in it. | ||||||
|  |      * | ||||||
|  |      * @param name The header of this section; e.g. [Core] or [Enhancements] without the brackets. | ||||||
|  |      */ | ||||||
|  |     public SettingSection(String name) { | ||||||
|  |         mName = name; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getName() { | ||||||
|  |         return mName; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Convenience method; inserts a value directly into the backing HashMap. | ||||||
|  |      * | ||||||
|  |      * @param setting The Setting to be inserted. | ||||||
|  |      */ | ||||||
|  |     public void putSetting(Setting setting) { | ||||||
|  |         mSettings.put(setting.getKey(), setting); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Convenience method; gets a value directly from the backing HashMap. | ||||||
|  |      * | ||||||
|  |      * @param key Used to retrieve the Setting. | ||||||
|  |      * @return A Setting object (you should probably cast this before using) | ||||||
|  |      */ | ||||||
|  |     public Setting getSetting(String key) { | ||||||
|  |         return mSettings.get(key); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public HashMap<String, Setting> getSettings() { | ||||||
|  |         return mSettings; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void mergeSection(SettingSection settingSection) { | ||||||
|  |         for (Setting setting : settingSection.mSettings.values()) { | ||||||
|  |             putSetting(setting); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,131 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model; | ||||||
|  | 
 | ||||||
|  | import android.text.TextUtils; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsActivityView; | ||||||
|  | import org.citra.citra_emu.features.settings.utils.SettingsFile; | ||||||
|  | 
 | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.TreeMap; | ||||||
|  | 
 | ||||||
|  | public class Settings { | ||||||
|  |     public static final String SECTION_PREMIUM = "Premium"; | ||||||
|  |     public static final String SECTION_CORE = "Core"; | ||||||
|  |     public static final String SECTION_SYSTEM = "System"; | ||||||
|  |     public static final String SECTION_CAMERA = "Camera"; | ||||||
|  |     public static final String SECTION_CONTROLS = "Controls"; | ||||||
|  |     public static final String SECTION_RENDERER = "Renderer"; | ||||||
|  |     public static final String SECTION_LAYOUT = "Layout"; | ||||||
|  |     public static final String SECTION_AUDIO = "Audio"; | ||||||
|  |     public static final String SECTION_DEBUG = "Debug"; | ||||||
|  | 
 | ||||||
|  |     private String gameId; | ||||||
|  | 
 | ||||||
|  |     private static final Map<String, List<String>> configFileSectionsMap = new HashMap<>(); | ||||||
|  | 
 | ||||||
|  |     static { | ||||||
|  |         configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_PREMIUM, SECTION_CORE, SECTION_SYSTEM, SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, SECTION_LAYOUT, SECTION_AUDIO, SECTION_DEBUG)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A HashMap<String, SettingSection> that constructs a new SettingSection instead of returning null | ||||||
|  |      * when getting a key not already in the map | ||||||
|  |      */ | ||||||
|  |     public static final class SettingsSectionMap extends HashMap<String, SettingSection> { | ||||||
|  |         @Override | ||||||
|  |         public SettingSection get(Object key) { | ||||||
|  |             if (!(key instanceof String)) { | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             String stringKey = (String) key; | ||||||
|  | 
 | ||||||
|  |             if (!super.containsKey(stringKey)) { | ||||||
|  |                 SettingSection section = new SettingSection(stringKey); | ||||||
|  |                 super.put(stringKey, section); | ||||||
|  |                 return section; | ||||||
|  |             } | ||||||
|  |             return super.get(key); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap(); | ||||||
|  | 
 | ||||||
|  |     public SettingSection getSection(String sectionName) { | ||||||
|  |         return sections.get(sectionName); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isEmpty() { | ||||||
|  |         return sections.isEmpty(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public HashMap<String, SettingSection> getSections() { | ||||||
|  |         return sections; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void loadSettings(SettingsActivityView view) { | ||||||
|  |         sections = new Settings.SettingsSectionMap(); | ||||||
|  |         loadCitraSettings(view); | ||||||
|  | 
 | ||||||
|  |         if (!TextUtils.isEmpty(gameId)) { | ||||||
|  |             loadCustomGameSettings(gameId, view); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void loadCitraSettings(SettingsActivityView view) { | ||||||
|  |         for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) { | ||||||
|  |             String fileName = entry.getKey(); | ||||||
|  |             sections.putAll(SettingsFile.readFile(fileName, view)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void loadCustomGameSettings(String gameId, SettingsActivityView view) { | ||||||
|  |         // custom game settings | ||||||
|  |         mergeSections(SettingsFile.readCustomGameSettings(gameId, view)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void mergeSections(HashMap<String, SettingSection> updatedSections) { | ||||||
|  |         for (Map.Entry<String, SettingSection> entry : updatedSections.entrySet()) { | ||||||
|  |             if (sections.containsKey(entry.getKey())) { | ||||||
|  |                 SettingSection originalSection = sections.get(entry.getKey()); | ||||||
|  |                 SettingSection updatedSection = entry.getValue(); | ||||||
|  |                 originalSection.mergeSection(updatedSection); | ||||||
|  |             } else { | ||||||
|  |                 sections.put(entry.getKey(), entry.getValue()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void loadSettings(String gameId, SettingsActivityView view) { | ||||||
|  |         this.gameId = gameId; | ||||||
|  |         loadSettings(view); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void saveSettings(SettingsActivityView view) { | ||||||
|  |         if (TextUtils.isEmpty(gameId)) { | ||||||
|  |             view.showToastMessage(CitraApplication.getAppContext().getString(R.string.ini_saved), false); | ||||||
|  | 
 | ||||||
|  |             for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) { | ||||||
|  |                 String fileName = entry.getKey(); | ||||||
|  |                 List<String> sectionNames = entry.getValue(); | ||||||
|  |                 TreeMap<String, SettingSection> iniSections = new TreeMap<>(); | ||||||
|  |                 for (String section : sectionNames) { | ||||||
|  |                     iniSections.put(section, sections.get(section)); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 SettingsFile.saveFile(fileName, iniSections, view); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             // custom game settings | ||||||
|  |             view.showToastMessage(CitraApplication.getAppContext().getString(R.string.gameid_saved, gameId), false); | ||||||
|  | 
 | ||||||
|  |             SettingsFile.saveCustomGameSettings(gameId, sections); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model; | ||||||
|  | 
 | ||||||
|  | public final class StringSetting extends Setting { | ||||||
|  |     private String mValue; | ||||||
|  | 
 | ||||||
|  |     public StringSetting(String key, String section, String value) { | ||||||
|  |         super(key, section); | ||||||
|  |         mValue = value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getValue() { | ||||||
|  |         return mValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setValue(String value) { | ||||||
|  |         mValue = value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String getValueAsString() { | ||||||
|  |         return mValue; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,80 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model.view; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.BooleanSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.IntSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsFragmentView; | ||||||
|  | 
 | ||||||
|  | public final class CheckBoxSetting extends SettingsItem { | ||||||
|  |     private boolean mDefaultValue; | ||||||
|  |     private boolean mShowPerformanceWarning; | ||||||
|  |     private SettingsFragmentView mView; | ||||||
|  | 
 | ||||||
|  |     public CheckBoxSetting(String key, String section, int titleId, int descriptionId, | ||||||
|  |                            boolean defaultValue, Setting setting) { | ||||||
|  |         super(key, section, setting, titleId, descriptionId); | ||||||
|  |         mDefaultValue = defaultValue; | ||||||
|  |         mShowPerformanceWarning = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public CheckBoxSetting(String key, String section, int titleId, int descriptionId, | ||||||
|  |                            boolean defaultValue, Setting setting, boolean show_performance_warning, SettingsFragmentView view) { | ||||||
|  |         super(key, section, setting, titleId, descriptionId); | ||||||
|  |         mDefaultValue = defaultValue; | ||||||
|  |         mView = view; | ||||||
|  |         mShowPerformanceWarning = show_performance_warning; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isChecked() { | ||||||
|  |         if (getSetting() == null) { | ||||||
|  |             return mDefaultValue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Try integer setting | ||||||
|  |         try { | ||||||
|  |             IntSetting setting = (IntSetting) getSetting(); | ||||||
|  |             return setting.getValue() == 1; | ||||||
|  |         } catch (ClassCastException exception) { | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Try boolean setting | ||||||
|  |         try { | ||||||
|  |             BooleanSetting setting = (BooleanSetting) getSetting(); | ||||||
|  |             return setting.getValue() == true; | ||||||
|  |         } catch (ClassCastException exception) { | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return mDefaultValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Write a value to the backing boolean. If that boolean was previously null, | ||||||
|  |      * initializes a new one and returns it, so it can be added to the Hashmap. | ||||||
|  |      * | ||||||
|  |      * @param checked Pretty self explanatory. | ||||||
|  |      * @return null if overwritten successfully; otherwise, a newly created BooleanSetting. | ||||||
|  |      */ | ||||||
|  |     public IntSetting setChecked(boolean checked) { | ||||||
|  |         // Show a performance warning if the setting has been disabled | ||||||
|  |         if (mShowPerformanceWarning && !checked) { | ||||||
|  |             mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.performance_warning), true); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (getSetting() == null) { | ||||||
|  |             IntSetting setting = new IntSetting(getKey(), getSection(), checked ? 1 : 0); | ||||||
|  |             setSetting(setting); | ||||||
|  |             return setting; | ||||||
|  |         } else { | ||||||
|  |             IntSetting setting = (IntSetting) getSetting(); | ||||||
|  |             setting.setValue(checked ? 1 : 0); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getType() { | ||||||
|  |         return TYPE_CHECKBOX; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,40 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model.view; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.StringSetting; | ||||||
|  | 
 | ||||||
|  | public final class DateTimeSetting extends SettingsItem { | ||||||
|  |     private String mDefaultValue; | ||||||
|  | 
 | ||||||
|  |     public DateTimeSetting(String key, String section, int titleId, int descriptionId, | ||||||
|  |                            String defaultValue, Setting setting) { | ||||||
|  |         super(key, section, setting, titleId, descriptionId); | ||||||
|  |         mDefaultValue = defaultValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getValue() { | ||||||
|  |         if (getSetting() != null) { | ||||||
|  |             StringSetting setting = (StringSetting) getSetting(); | ||||||
|  |             return setting.getValue(); | ||||||
|  |         } else { | ||||||
|  |             return mDefaultValue; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public StringSetting setSelectedValue(String datetime) { | ||||||
|  |         if (getSetting() == null) { | ||||||
|  |             StringSetting setting = new StringSetting(getKey(), getSection(), datetime); | ||||||
|  |             setSetting(setting); | ||||||
|  |             return setting; | ||||||
|  |         } else { | ||||||
|  |             StringSetting setting = (StringSetting) getSetting(); | ||||||
|  |             setting.setValue(datetime); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getType() { | ||||||
|  |         return TYPE_DATETIME_SETTING; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,14 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model.view; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | 
 | ||||||
|  | public final class HeaderSetting extends SettingsItem { | ||||||
|  |     public HeaderSetting(String key, Setting setting, int titleId, int descriptionId) { | ||||||
|  |         super(key, null, setting, titleId, descriptionId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getType() { | ||||||
|  |         return SettingsItem.TYPE_HEADER; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,382 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model.view; | ||||||
|  | 
 | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | import android.view.InputDevice; | ||||||
|  | import android.view.KeyEvent; | ||||||
|  | import android.widget.Toast; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.StringSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.utils.SettingsFile; | ||||||
|  | 
 | ||||||
|  | public final class InputBindingSetting extends SettingsItem { | ||||||
|  |     private static final String INPUT_MAPPING_PREFIX = "InputMapping"; | ||||||
|  | 
 | ||||||
|  |     public InputBindingSetting(String key, String section, int titleId, Setting setting) { | ||||||
|  |         super(key, section, setting, titleId, 0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getValue() { | ||||||
|  |         if (getSetting() == null) { | ||||||
|  |             return ""; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         StringSetting setting = (StringSetting) getSetting(); | ||||||
|  |         return setting.getValue(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns true if this key is for the 3DS Circle Pad | ||||||
|  |      */ | ||||||
|  |     private boolean IsCirclePad() { | ||||||
|  |         switch (getKey()) { | ||||||
|  |             case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL: | ||||||
|  |             case SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL: | ||||||
|  |                 return true; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad | ||||||
|  |      */ | ||||||
|  |     public boolean IsHorizontalOrientation() { | ||||||
|  |         switch (getKey()) { | ||||||
|  |             case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL: | ||||||
|  |             case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL: | ||||||
|  |             case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL: | ||||||
|  |                 return true; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns true if this key is for the 3DS C-Stick | ||||||
|  |      */ | ||||||
|  |     private boolean IsCStick() { | ||||||
|  |         switch (getKey()) { | ||||||
|  |             case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL: | ||||||
|  |             case SettingsFile.KEY_CSTICK_AXIS_VERTICAL: | ||||||
|  |                 return true; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns true if this key is for the 3DS D-Pad | ||||||
|  |      */ | ||||||
|  |     private boolean IsDPad() { | ||||||
|  |         switch (getKey()) { | ||||||
|  |             case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL: | ||||||
|  |             case SettingsFile.KEY_DPAD_AXIS_VERTICAL: | ||||||
|  |                 return true; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real | ||||||
|  |      * triggers on the 3DS, but we support them as such on a physical gamepad. | ||||||
|  |      */ | ||||||
|  |     public boolean IsTrigger() { | ||||||
|  |         switch (getKey()) { | ||||||
|  |             case SettingsFile.KEY_BUTTON_L: | ||||||
|  |             case SettingsFile.KEY_BUTTON_R: | ||||||
|  |             case SettingsFile.KEY_BUTTON_ZL: | ||||||
|  |             case SettingsFile.KEY_BUTTON_ZR: | ||||||
|  |                 return true; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns true if a gamepad axis can be used to map this key. | ||||||
|  |      */ | ||||||
|  |     public boolean IsAxisMappingSupported() { | ||||||
|  |         return IsCirclePad() || IsCStick() || IsDPad() || IsTrigger(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns true if a gamepad button can be used to map this key. | ||||||
|  |      */ | ||||||
|  |     private boolean IsButtonMappingSupported() { | ||||||
|  |         return !IsAxisMappingSupported() || IsTrigger(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns the Citra button code for the settings key. | ||||||
|  |      */ | ||||||
|  |     private int getButtonCode() { | ||||||
|  |         switch (getKey()) { | ||||||
|  |             case SettingsFile.KEY_BUTTON_A: | ||||||
|  |                 return NativeLibrary.ButtonType.BUTTON_A; | ||||||
|  |             case SettingsFile.KEY_BUTTON_B: | ||||||
|  |                 return NativeLibrary.ButtonType.BUTTON_B; | ||||||
|  |             case SettingsFile.KEY_BUTTON_X: | ||||||
|  |                 return NativeLibrary.ButtonType.BUTTON_X; | ||||||
|  |             case SettingsFile.KEY_BUTTON_Y: | ||||||
|  |                 return NativeLibrary.ButtonType.BUTTON_Y; | ||||||
|  |             case SettingsFile.KEY_BUTTON_L: | ||||||
|  |                 return NativeLibrary.ButtonType.TRIGGER_L; | ||||||
|  |             case SettingsFile.KEY_BUTTON_R: | ||||||
|  |                 return NativeLibrary.ButtonType.TRIGGER_R; | ||||||
|  |             case SettingsFile.KEY_BUTTON_ZL: | ||||||
|  |                 return NativeLibrary.ButtonType.BUTTON_ZL; | ||||||
|  |             case SettingsFile.KEY_BUTTON_ZR: | ||||||
|  |                 return NativeLibrary.ButtonType.BUTTON_ZR; | ||||||
|  |             case SettingsFile.KEY_BUTTON_SELECT: | ||||||
|  |                 return NativeLibrary.ButtonType.BUTTON_SELECT; | ||||||
|  |             case SettingsFile.KEY_BUTTON_START: | ||||||
|  |                 return NativeLibrary.ButtonType.BUTTON_START; | ||||||
|  |             case SettingsFile.KEY_BUTTON_UP: | ||||||
|  |                 return NativeLibrary.ButtonType.DPAD_UP; | ||||||
|  |             case SettingsFile.KEY_BUTTON_DOWN: | ||||||
|  |                 return NativeLibrary.ButtonType.DPAD_DOWN; | ||||||
|  |             case SettingsFile.KEY_BUTTON_LEFT: | ||||||
|  |                 return NativeLibrary.ButtonType.DPAD_LEFT; | ||||||
|  |             case SettingsFile.KEY_BUTTON_RIGHT: | ||||||
|  |                 return NativeLibrary.ButtonType.DPAD_RIGHT; | ||||||
|  |         } | ||||||
|  |         return -1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns the settings key for the specified Citra button code. | ||||||
|  |      */ | ||||||
|  |     private static String getButtonKey(int buttonCode) { | ||||||
|  |         switch (buttonCode) { | ||||||
|  |             case NativeLibrary.ButtonType.BUTTON_A: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_A; | ||||||
|  |             case NativeLibrary.ButtonType.BUTTON_B: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_B; | ||||||
|  |             case NativeLibrary.ButtonType.BUTTON_X: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_X; | ||||||
|  |             case NativeLibrary.ButtonType.BUTTON_Y: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_Y; | ||||||
|  |             case NativeLibrary.ButtonType.TRIGGER_L: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_L; | ||||||
|  |             case NativeLibrary.ButtonType.TRIGGER_R: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_R; | ||||||
|  |             case NativeLibrary.ButtonType.BUTTON_ZL: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_ZL; | ||||||
|  |             case NativeLibrary.ButtonType.BUTTON_ZR: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_ZR; | ||||||
|  |             case NativeLibrary.ButtonType.BUTTON_SELECT: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_SELECT; | ||||||
|  |             case NativeLibrary.ButtonType.BUTTON_START: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_START; | ||||||
|  |             case NativeLibrary.ButtonType.DPAD_UP: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_UP; | ||||||
|  |             case NativeLibrary.ButtonType.DPAD_DOWN: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_DOWN; | ||||||
|  |             case NativeLibrary.ButtonType.DPAD_LEFT: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_LEFT; | ||||||
|  |             case NativeLibrary.ButtonType.DPAD_RIGHT: | ||||||
|  |                 return SettingsFile.KEY_BUTTON_RIGHT; | ||||||
|  |         } | ||||||
|  |         return ""; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old | ||||||
|  |      * settings on re-mapping or clearing of a setting. | ||||||
|  |      */ | ||||||
|  |     private String getReverseKey() { | ||||||
|  |         String reverseKey = INPUT_MAPPING_PREFIX + "_ReverseMapping_" + getKey(); | ||||||
|  | 
 | ||||||
|  |         if (IsAxisMappingSupported() && !IsTrigger()) { | ||||||
|  |             // Triggers are the only axis-supported mappings without orientation | ||||||
|  |             reverseKey += "_" + (IsHorizontalOrientation() ? 0 : 1); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return reverseKey; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Removes the old mapping for this key from the settings, e.g. on user clearing the setting. | ||||||
|  |      */ | ||||||
|  |     public void removeOldMapping() { | ||||||
|  |         // Get preferences editor | ||||||
|  |         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); | ||||||
|  |         SharedPreferences.Editor editor = preferences.edit(); | ||||||
|  | 
 | ||||||
|  |         // Try remove all possible keys we wrote for this setting | ||||||
|  |         String oldKey = preferences.getString(getReverseKey(), ""); | ||||||
|  |         if (!oldKey.equals("")) { | ||||||
|  |             editor.remove(getKey()); // Used for ui text | ||||||
|  |             editor.remove(oldKey); // Used for button mapping | ||||||
|  |             editor.remove(oldKey + "_GuestOrientation"); // Used for axis orientation | ||||||
|  |             editor.remove(oldKey + "_GuestButton"); // Used for axis button | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Apply changes | ||||||
|  |         editor.apply(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Helper function to get the settings key for an gamepad button. | ||||||
|  |      */ | ||||||
|  |     public static String getInputButtonKey(int keyCode) { | ||||||
|  |         return INPUT_MAPPING_PREFIX + "_Button_" + keyCode; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Helper function to get the settings key for an gamepad axis. | ||||||
|  |      */ | ||||||
|  |     public static String getInputAxisKey(int axis) { | ||||||
|  |         return INPUT_MAPPING_PREFIX + "_HostAxis_" + axis; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Helper function to get the settings key for an gamepad axis button (stick or trigger). | ||||||
|  |      */ | ||||||
|  |     public static String getInputAxisButtonKey(int axis) { | ||||||
|  |         return getInputAxisKey(axis) + "_GuestButton"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Helper function to get the settings key for an gamepad axis orientation. | ||||||
|  |      */ | ||||||
|  |     public static String getInputAxisOrientationKey(int axis) { | ||||||
|  |         return getInputAxisKey(axis) + "_GuestOrientation"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Helper function to write a gamepad button mapping for the setting. | ||||||
|  |      */ | ||||||
|  |     private void WriteButtonMapping(String key) { | ||||||
|  |         // Get preferences editor | ||||||
|  |         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); | ||||||
|  |         SharedPreferences.Editor editor = preferences.edit(); | ||||||
|  | 
 | ||||||
|  |         // Remove mapping for another setting using this input | ||||||
|  |         int oldButtonCode = preferences.getInt(key, -1); | ||||||
|  |         if (oldButtonCode != -1) { | ||||||
|  |             String oldKey = getButtonKey(oldButtonCode); | ||||||
|  |             editor.remove(oldKey); // Only need to remove UI text setting, others will be overwritten | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Cleanup old mapping for this setting | ||||||
|  |         removeOldMapping(); | ||||||
|  | 
 | ||||||
|  |         // Write new mapping | ||||||
|  |         editor.putInt(key, getButtonCode()); | ||||||
|  | 
 | ||||||
|  |         // Write next reverse mapping for future cleanup | ||||||
|  |         editor.putString(getReverseKey(), key); | ||||||
|  | 
 | ||||||
|  |         // Apply changes | ||||||
|  |         editor.apply(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Helper function to write a gamepad axis mapping for the setting. | ||||||
|  |      */ | ||||||
|  |     private void WriteAxisMapping(int axis, int value) { | ||||||
|  |         // Get preferences editor | ||||||
|  |         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); | ||||||
|  |         SharedPreferences.Editor editor = preferences.edit(); | ||||||
|  | 
 | ||||||
|  |         // Cleanup old mapping | ||||||
|  |         removeOldMapping(); | ||||||
|  | 
 | ||||||
|  |         // Write new mapping | ||||||
|  |         editor.putInt(getInputAxisOrientationKey(axis), IsHorizontalOrientation() ? 0 : 1); | ||||||
|  |         editor.putInt(getInputAxisButtonKey(axis), value); | ||||||
|  | 
 | ||||||
|  |         // Write next reverse mapping for future cleanup | ||||||
|  |         editor.putString(getReverseKey(), getInputAxisKey(axis)); | ||||||
|  | 
 | ||||||
|  |         // Apply changes | ||||||
|  |         editor.apply(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Saves the provided key input setting as an Android preference. | ||||||
|  |      * | ||||||
|  |      * @param keyEvent KeyEvent of this key press. | ||||||
|  |      */ | ||||||
|  |     public void onKeyInput(KeyEvent keyEvent) { | ||||||
|  |         if (!IsButtonMappingSupported()) { | ||||||
|  |             Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         InputDevice device = keyEvent.getDevice(); | ||||||
|  | 
 | ||||||
|  |         WriteButtonMapping(getInputButtonKey(keyEvent.getKeyCode())); | ||||||
|  | 
 | ||||||
|  |         String uiString = device.getName() + ": Button " + keyEvent.getKeyCode(); | ||||||
|  |         setUiString(uiString); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Saves the provided motion input setting as an Android preference. | ||||||
|  |      * | ||||||
|  |      * @param device      InputDevice from which the input event originated. | ||||||
|  |      * @param motionRange MotionRange of the movement | ||||||
|  |      * @param axisDir     Either '-' or '+' (currently unused) | ||||||
|  |      */ | ||||||
|  |     public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange, | ||||||
|  |                               char axisDir) { | ||||||
|  |         if (!IsAxisMappingSupported()) { | ||||||
|  |             Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); | ||||||
|  |         SharedPreferences.Editor editor = preferences.edit(); | ||||||
|  | 
 | ||||||
|  |         int button; | ||||||
|  |         if (IsCirclePad()) { | ||||||
|  |             button = NativeLibrary.ButtonType.STICK_LEFT; | ||||||
|  |         } else if (IsCStick()) { | ||||||
|  |             button = NativeLibrary.ButtonType.STICK_C; | ||||||
|  |         } else if (IsDPad()) { | ||||||
|  |             button = NativeLibrary.ButtonType.DPAD; | ||||||
|  |         } else { | ||||||
|  |             button = getButtonCode(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         WriteAxisMapping(motionRange.getAxis(), button); | ||||||
|  | 
 | ||||||
|  |         String uiString = device.getName() + ": Axis " + motionRange.getAxis(); | ||||||
|  |         setUiString(uiString); | ||||||
|  | 
 | ||||||
|  |         editor.apply(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Sets the string to use in the configuration UI for the gamepad input. | ||||||
|  |      */ | ||||||
|  |     private StringSetting setUiString(String ui) { | ||||||
|  |         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); | ||||||
|  |         SharedPreferences.Editor editor = preferences.edit(); | ||||||
|  | 
 | ||||||
|  |         if (getSetting() == null) { | ||||||
|  |             StringSetting setting = new StringSetting(getKey(), getSection(), ""); | ||||||
|  |             setSetting(setting); | ||||||
|  | 
 | ||||||
|  |             editor.putString(setting.getKey(), ui); | ||||||
|  |             editor.apply(); | ||||||
|  | 
 | ||||||
|  |             return setting; | ||||||
|  |         } else { | ||||||
|  |             StringSetting setting = (StringSetting) getSetting(); | ||||||
|  | 
 | ||||||
|  |             editor.putString(setting.getKey(), ui); | ||||||
|  |             editor.apply(); | ||||||
|  | 
 | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getType() { | ||||||
|  |         return TYPE_INPUT_BINDING; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,14 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model.view; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | 
 | ||||||
|  | public final class PremiumHeader extends SettingsItem { | ||||||
|  |     public PremiumHeader() { | ||||||
|  |         super(null, null, null, 0, 0); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getType() { | ||||||
|  |         return SettingsItem.TYPE_PREMIUM; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,59 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model.view; | ||||||
|  | 
 | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsFragmentView; | ||||||
|  | 
 | ||||||
|  | public final class PremiumSingleChoiceSetting extends SettingsItem { | ||||||
|  |     private int mDefaultValue; | ||||||
|  | 
 | ||||||
|  |     private int mChoicesId; | ||||||
|  |     private int mValuesId; | ||||||
|  |     private SettingsFragmentView mView; | ||||||
|  | 
 | ||||||
|  |     private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); | ||||||
|  | 
 | ||||||
|  |     public PremiumSingleChoiceSetting(String key, String section, int titleId, int descriptionId, | ||||||
|  |                                       int choicesId, int valuesId, int defaultValue, Setting setting, SettingsFragmentView view) { | ||||||
|  |         super(key, section, setting, titleId, descriptionId); | ||||||
|  |         mValuesId = valuesId; | ||||||
|  |         mChoicesId = choicesId; | ||||||
|  |         mDefaultValue = defaultValue; | ||||||
|  |         mView = view; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getChoicesId() { | ||||||
|  |         return mChoicesId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getValuesId() { | ||||||
|  |         return mValuesId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getSelectedValue() { | ||||||
|  |         return mPreferences.getInt(getKey(), mDefaultValue); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Write a value to the backing int. If that int was previously null, | ||||||
|  |      * initializes a new one and returns it, so it can be added to the Hashmap. | ||||||
|  |      * | ||||||
|  |      * @param selection New value of the int. | ||||||
|  |      * @return null if overwritten successfully otherwise; a newly created IntSetting. | ||||||
|  |      */ | ||||||
|  |     public void setSelectedValue(int selection) { | ||||||
|  |         final SharedPreferences.Editor editor = mPreferences.edit(); | ||||||
|  |         editor.putInt(getKey(), selection); | ||||||
|  |         editor.apply(); | ||||||
|  |         mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.design_updated), false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getType() { | ||||||
|  |         return TYPE_SINGLE_CHOICE; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,107 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model.view; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Settings; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsAdapter; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. | ||||||
|  |  * Each one corresponds to a {@link Setting} object, so this class's subclasses | ||||||
|  |  * should vaguely correspond to those subclasses. There are a few with multiple analogues | ||||||
|  |  * and a few with none (Headers, for example, do not correspond to anything in the ini | ||||||
|  |  * file.) | ||||||
|  |  */ | ||||||
|  | public abstract class SettingsItem { | ||||||
|  |     public static final int TYPE_HEADER = 0; | ||||||
|  |     public static final int TYPE_CHECKBOX = 1; | ||||||
|  |     public static final int TYPE_SINGLE_CHOICE = 2; | ||||||
|  |     public static final int TYPE_SLIDER = 3; | ||||||
|  |     public static final int TYPE_SUBMENU = 4; | ||||||
|  |     public static final int TYPE_INPUT_BINDING = 5; | ||||||
|  |     public static final int TYPE_STRING_SINGLE_CHOICE = 6; | ||||||
|  |     public static final int TYPE_DATETIME_SETTING = 7; | ||||||
|  |     public static final int TYPE_PREMIUM = 8; | ||||||
|  | 
 | ||||||
|  |     private String mKey; | ||||||
|  |     private String mSection; | ||||||
|  | 
 | ||||||
|  |     private Setting mSetting; | ||||||
|  | 
 | ||||||
|  |     private int mNameId; | ||||||
|  |     private int mDescriptionId; | ||||||
|  |     private boolean mIsPremium; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Base constructor. Takes a key / section name in case the third parameter, the Setting, | ||||||
|  |      * is null; in which case, one can be constructed and saved using the key / section. | ||||||
|  |      * | ||||||
|  |      * @param key           Identifier for the Setting represented by this Item. | ||||||
|  |      * @param section       Section to which the Setting belongs. | ||||||
|  |      * @param setting       A possibly-null backing Setting, to be modified on UI events. | ||||||
|  |      * @param nameId        Resource ID for a text string to be displayed as this setting's name. | ||||||
|  |      * @param descriptionId Resource ID for a text string to be displayed as this setting's description. | ||||||
|  |      */ | ||||||
|  |     public SettingsItem(String key, String section, Setting setting, int nameId, | ||||||
|  |                         int descriptionId) { | ||||||
|  |         mKey = key; | ||||||
|  |         mSection = section; | ||||||
|  |         mSetting = setting; | ||||||
|  |         mNameId = nameId; | ||||||
|  |         mDescriptionId = descriptionId; | ||||||
|  |         mIsPremium = (section == Settings.SECTION_PREMIUM); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return The identifier for the backing Setting. | ||||||
|  |      */ | ||||||
|  |     public String getKey() { | ||||||
|  |         return mKey; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return The header under which the backing Setting belongs. | ||||||
|  |      */ | ||||||
|  |     public String getSection() { | ||||||
|  |         return mSection; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return The backing Setting, possibly null. | ||||||
|  |      */ | ||||||
|  |     public Setting getSetting() { | ||||||
|  |         return mSetting; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Replace the backing setting with a new one. Generally used in cases where | ||||||
|  |      * the backing setting is null. | ||||||
|  |      * | ||||||
|  |      * @param setting A non-null Setting. | ||||||
|  |      */ | ||||||
|  |     public void setSetting(Setting setting) { | ||||||
|  |         mSetting = setting; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return A resource ID for a text string representing this Setting's name. | ||||||
|  |      */ | ||||||
|  |     public int getNameId() { | ||||||
|  |         return mNameId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getDescriptionId() { | ||||||
|  |         return mDescriptionId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isPremium() { | ||||||
|  |         return mIsPremium; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Used by {@link SettingsAdapter}'s onCreateViewHolder() | ||||||
|  |      * method to determine which type of ViewHolder should be created. | ||||||
|  |      * | ||||||
|  |      * @return An integer (ideally, one of the constants defined in this file) | ||||||
|  |      */ | ||||||
|  |     public abstract int getType(); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,60 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model.view; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.IntSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | 
 | ||||||
|  | public final class SingleChoiceSetting extends SettingsItem { | ||||||
|  |     private int mDefaultValue; | ||||||
|  | 
 | ||||||
|  |     private int mChoicesId; | ||||||
|  |     private int mValuesId; | ||||||
|  | 
 | ||||||
|  |     public SingleChoiceSetting(String key, String section, int titleId, int descriptionId, | ||||||
|  |                                int choicesId, int valuesId, int defaultValue, Setting setting) { | ||||||
|  |         super(key, section, setting, titleId, descriptionId); | ||||||
|  |         mValuesId = valuesId; | ||||||
|  |         mChoicesId = choicesId; | ||||||
|  |         mDefaultValue = defaultValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getChoicesId() { | ||||||
|  |         return mChoicesId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getValuesId() { | ||||||
|  |         return mValuesId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getSelectedValue() { | ||||||
|  |         if (getSetting() != null) { | ||||||
|  |             IntSetting setting = (IntSetting) getSetting(); | ||||||
|  |             return setting.getValue(); | ||||||
|  |         } else { | ||||||
|  |             return mDefaultValue; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Write a value to the backing int. If that int was previously null, | ||||||
|  |      * initializes a new one and returns it, so it can be added to the Hashmap. | ||||||
|  |      * | ||||||
|  |      * @param selection New value of the int. | ||||||
|  |      * @return null if overwritten successfully otherwise; a newly created IntSetting. | ||||||
|  |      */ | ||||||
|  |     public IntSetting setSelectedValue(int selection) { | ||||||
|  |         if (getSetting() == null) { | ||||||
|  |             IntSetting setting = new IntSetting(getKey(), getSection(), selection); | ||||||
|  |             setSetting(setting); | ||||||
|  |             return setting; | ||||||
|  |         } else { | ||||||
|  |             IntSetting setting = (IntSetting) getSetting(); | ||||||
|  |             setting.setValue(selection); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getType() { | ||||||
|  |         return TYPE_SINGLE_CHOICE; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,101 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model.view; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.FloatSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.IntSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | 
 | ||||||
|  | public final class SliderSetting extends SettingsItem { | ||||||
|  |     private int mMin; | ||||||
|  |     private int mMax; | ||||||
|  |     private int mDefaultValue; | ||||||
|  | 
 | ||||||
|  |     private String mUnits; | ||||||
|  | 
 | ||||||
|  |     public SliderSetting(String key, String section, int titleId, int descriptionId, | ||||||
|  |                          int min, int max, String units, int defaultValue, Setting setting) { | ||||||
|  |         super(key, section, setting, titleId, descriptionId); | ||||||
|  |         mMin = min; | ||||||
|  |         mMax = max; | ||||||
|  |         mUnits = units; | ||||||
|  |         mDefaultValue = defaultValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getMin() { | ||||||
|  |         return mMin; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getMax() { | ||||||
|  |         return mMax; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getDefaultValue() { | ||||||
|  |         return mDefaultValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getSelectedValue() { | ||||||
|  |         Setting setting = getSetting(); | ||||||
|  | 
 | ||||||
|  |         if (setting == null) { | ||||||
|  |             return mDefaultValue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (setting instanceof IntSetting) { | ||||||
|  |             IntSetting intSetting = (IntSetting) setting; | ||||||
|  |             return intSetting.getValue(); | ||||||
|  |         } else if (setting instanceof FloatSetting) { | ||||||
|  |             FloatSetting floatSetting = (FloatSetting) setting; | ||||||
|  |             return Math.round(floatSetting.getValue()); | ||||||
|  |         } else { | ||||||
|  |             Log.error("[SliderSetting] Error casting setting type."); | ||||||
|  |             return -1; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Write a value to the backing int. If that int was previously null, | ||||||
|  |      * initializes a new one and returns it, so it can be added to the Hashmap. | ||||||
|  |      * | ||||||
|  |      * @param selection New value of the int. | ||||||
|  |      * @return null if overwritten successfully otherwise; a newly created IntSetting. | ||||||
|  |      */ | ||||||
|  |     public IntSetting setSelectedValue(int selection) { | ||||||
|  |         if (getSetting() == null) { | ||||||
|  |             IntSetting setting = new IntSetting(getKey(), getSection(), selection); | ||||||
|  |             setSetting(setting); | ||||||
|  |             return setting; | ||||||
|  |         } else { | ||||||
|  |             IntSetting setting = (IntSetting) getSetting(); | ||||||
|  |             setting.setValue(selection); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Write a value to the backing float. If that float was previously null, | ||||||
|  |      * initializes a new one and returns it, so it can be added to the Hashmap. | ||||||
|  |      * | ||||||
|  |      * @param selection New value of the float. | ||||||
|  |      * @return null if overwritten successfully otherwise; a newly created FloatSetting. | ||||||
|  |      */ | ||||||
|  |     public FloatSetting setSelectedValue(float selection) { | ||||||
|  |         if (getSetting() == null) { | ||||||
|  |             FloatSetting setting = new FloatSetting(getKey(), getSection(), selection); | ||||||
|  |             setSetting(setting); | ||||||
|  |             return setting; | ||||||
|  |         } else { | ||||||
|  |             FloatSetting setting = (FloatSetting) getSetting(); | ||||||
|  |             setting.setValue(selection); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getUnits() { | ||||||
|  |         return mUnits; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getType() { | ||||||
|  |         return TYPE_SLIDER; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,82 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model.view; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.StringSetting; | ||||||
|  | 
 | ||||||
|  | public class StringSingleChoiceSetting extends SettingsItem { | ||||||
|  |     private String mDefaultValue; | ||||||
|  | 
 | ||||||
|  |     private String[] mChoicesId; | ||||||
|  |     private String[] mValuesId; | ||||||
|  | 
 | ||||||
|  |     public StringSingleChoiceSetting(String key, String section, int titleId, int descriptionId, | ||||||
|  |                                      String[] choicesId, String[] valuesId, String defaultValue, Setting setting) { | ||||||
|  |         super(key, section, setting, titleId, descriptionId); | ||||||
|  |         mValuesId = valuesId; | ||||||
|  |         mChoicesId = choicesId; | ||||||
|  |         mDefaultValue = defaultValue; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String[] getChoicesId() { | ||||||
|  |         return mChoicesId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String[] getValuesId() { | ||||||
|  |         return mValuesId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getValueAt(int index) { | ||||||
|  |         if (mValuesId == null) | ||||||
|  |             return null; | ||||||
|  | 
 | ||||||
|  |         if (index >= 0 && index < mValuesId.length) { | ||||||
|  |             return mValuesId[index]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return ""; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getSelectedValue() { | ||||||
|  |         if (getSetting() != null) { | ||||||
|  |             StringSetting setting = (StringSetting) getSetting(); | ||||||
|  |             return setting.getValue(); | ||||||
|  |         } else { | ||||||
|  |             return mDefaultValue; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getSelectValueIndex() { | ||||||
|  |         String selectedValue = getSelectedValue(); | ||||||
|  |         for (int i = 0; i < mValuesId.length; i++) { | ||||||
|  |             if (mValuesId[i].equals(selectedValue)) { | ||||||
|  |                 return i; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return -1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Write a value to the backing int. If that int was previously null, | ||||||
|  |      * initializes a new one and returns it, so it can be added to the Hashmap. | ||||||
|  |      * | ||||||
|  |      * @param selection New value of the int. | ||||||
|  |      * @return null if overwritten successfully otherwise; a newly created IntSetting. | ||||||
|  |      */ | ||||||
|  |     public StringSetting setSelectedValue(String selection) { | ||||||
|  |         if (getSetting() == null) { | ||||||
|  |             StringSetting setting = new StringSetting(getKey(), getSection(), selection); | ||||||
|  |             setSetting(setting); | ||||||
|  |             return setting; | ||||||
|  |         } else { | ||||||
|  |             StringSetting setting = (StringSetting) getSetting(); | ||||||
|  |             setting.setValue(selection); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getType() { | ||||||
|  |         return TYPE_STRING_SINGLE_CHOICE; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,21 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.model.view; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | 
 | ||||||
|  | public final class SubmenuSetting extends SettingsItem { | ||||||
|  |     private String mMenuKey; | ||||||
|  | 
 | ||||||
|  |     public SubmenuSetting(String key, Setting setting, int titleId, int descriptionId, String menuKey) { | ||||||
|  |         super(key, null, setting, titleId, descriptionId); | ||||||
|  |         mMenuKey = menuKey; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getMenuKey() { | ||||||
|  |         return mMenuKey; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getType() { | ||||||
|  |         return TYPE_SUBMENU; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,215 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui; | ||||||
|  | 
 | ||||||
|  | import android.app.ProgressDialog; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.content.IntentFilter; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.provider.Settings; | ||||||
|  | import android.view.Menu; | ||||||
|  | import android.view.MenuInflater; | ||||||
|  | import android.widget.Toast; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.appcompat.app.AppCompatActivity; | ||||||
|  | import androidx.fragment.app.FragmentTransaction; | ||||||
|  | import androidx.localbroadcastmanager.content.LocalBroadcastManager; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryInitialization; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryStateReceiver; | ||||||
|  | import org.citra.citra_emu.utils.EmulationMenuSettings; | ||||||
|  | 
 | ||||||
|  | public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView { | ||||||
|  |     private static final String ARG_MENU_TAG = "menu_tag"; | ||||||
|  |     private static final String ARG_GAME_ID = "game_id"; | ||||||
|  |     private static final String FRAGMENT_TAG = "settings"; | ||||||
|  |     private SettingsActivityPresenter mPresenter = new SettingsActivityPresenter(this); | ||||||
|  | 
 | ||||||
|  |     private ProgressDialog dialog; | ||||||
|  | 
 | ||||||
|  |     public static void launch(Context context, String menuTag, String gameId) { | ||||||
|  |         Intent settings = new Intent(context, SettingsActivity.class); | ||||||
|  |         settings.putExtra(ARG_MENU_TAG, menuTag); | ||||||
|  |         settings.putExtra(ARG_GAME_ID, gameId); | ||||||
|  |         context.startActivity(settings); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  | 
 | ||||||
|  |         setContentView(R.layout.activity_settings); | ||||||
|  | 
 | ||||||
|  |         Intent launcher = getIntent(); | ||||||
|  |         String gameID = launcher.getStringExtra(ARG_GAME_ID); | ||||||
|  |         String menuTag = launcher.getStringExtra(ARG_MENU_TAG); | ||||||
|  | 
 | ||||||
|  |         mPresenter.onCreate(savedInstanceState, menuTag, gameID); | ||||||
|  | 
 | ||||||
|  |         // Show "Back" button in the action bar for navigation | ||||||
|  |         getSupportActionBar().setDisplayHomeAsUpEnabled(true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onSupportNavigateUp() { | ||||||
|  |         onBackPressed(); | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onCreateOptionsMenu(Menu menu) { | ||||||
|  |         MenuInflater inflater = getMenuInflater(); | ||||||
|  |         inflater.inflate(R.menu.menu_settings, menu); | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onSaveInstanceState(@NonNull Bundle outState) { | ||||||
|  |         // Critical: If super method is not called, rotations will be busted. | ||||||
|  |         super.onSaveInstanceState(outState); | ||||||
|  |         mPresenter.saveState(outState); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onStart() { | ||||||
|  |         super.onStart(); | ||||||
|  |         mPresenter.onStart(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * If this is called, the user has left the settings screen (potentially through the | ||||||
|  |      * home button) and will expect their changes to be persisted. So we kick off an | ||||||
|  |      * IntentService which will do so on a background thread. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     protected void onStop() { | ||||||
|  |         super.onStop(); | ||||||
|  | 
 | ||||||
|  |         mPresenter.onStop(isFinishing()); | ||||||
|  | 
 | ||||||
|  |         // Update framebuffer layout when closing the settings | ||||||
|  |         NativeLibrary.NotifyOrientationChange(EmulationMenuSettings.getLandscapeScreenLayout(), | ||||||
|  |                 getWindowManager().getDefaultDisplay().getRotation()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void showSettingsFragment(String menuTag, boolean addToStack, String gameID) { | ||||||
|  |         if (!addToStack && getFragment() != null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); | ||||||
|  | 
 | ||||||
|  |         if (addToStack) { | ||||||
|  |             if (areSystemAnimationsEnabled()) { | ||||||
|  |                 transaction.setCustomAnimations( | ||||||
|  |                         R.animator.settings_enter, | ||||||
|  |                         R.animator.settings_exit, | ||||||
|  |                         R.animator.settings_pop_enter, | ||||||
|  |                         R.animator.setttings_pop_exit); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             transaction.addToBackStack(null); | ||||||
|  |         } | ||||||
|  |         transaction.replace(R.id.frame_content, SettingsFragment.newInstance(menuTag, gameID), FRAGMENT_TAG); | ||||||
|  | 
 | ||||||
|  |         transaction.commit(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean areSystemAnimationsEnabled() { | ||||||
|  |         float duration = Settings.Global.getFloat( | ||||||
|  |                 getContentResolver(), | ||||||
|  |                 Settings.Global.ANIMATOR_DURATION_SCALE, 1); | ||||||
|  |         float transition = Settings.Global.getFloat( | ||||||
|  |                 getContentResolver(), | ||||||
|  |                 Settings.Global.TRANSITION_ANIMATION_SCALE, 1); | ||||||
|  |         return duration != 0 && transition != 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter) { | ||||||
|  |         LocalBroadcastManager.getInstance(this).registerReceiver( | ||||||
|  |                 receiver, | ||||||
|  |                 filter); | ||||||
|  |         DirectoryInitialization.start(this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver) { | ||||||
|  |         LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void showLoading() { | ||||||
|  |         if (dialog == null) { | ||||||
|  |             dialog = new ProgressDialog(this); | ||||||
|  |             dialog.setMessage(getString(R.string.load_settings)); | ||||||
|  |             dialog.setIndeterminate(true); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         dialog.show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void hideLoading() { | ||||||
|  |         dialog.dismiss(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void showPermissionNeededHint() { | ||||||
|  |         Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT) | ||||||
|  |                 .show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void showExternalStorageNotMountedHint() { | ||||||
|  |         Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT) | ||||||
|  |                 .show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public org.citra.citra_emu.features.settings.model.Settings getSettings() { | ||||||
|  |         return mPresenter.getSettings(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void setSettings(org.citra.citra_emu.features.settings.model.Settings settings) { | ||||||
|  |         mPresenter.setSettings(settings); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onSettingsFileLoaded(org.citra.citra_emu.features.settings.model.Settings settings) { | ||||||
|  |         SettingsFragmentView fragment = getFragment(); | ||||||
|  | 
 | ||||||
|  |         if (fragment != null) { | ||||||
|  |             fragment.onSettingsFileLoaded(settings); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onSettingsFileNotFound() { | ||||||
|  |         SettingsFragmentView fragment = getFragment(); | ||||||
|  | 
 | ||||||
|  |         if (fragment != null) { | ||||||
|  |             fragment.loadDefaultSettings(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void showToastMessage(String message, boolean is_long) { | ||||||
|  |         Toast.makeText(this, message, is_long ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onSettingChanged() { | ||||||
|  |         mPresenter.onSettingChanged(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private SettingsFragment getFragment() { | ||||||
|  |         return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,124 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui; | ||||||
|  | 
 | ||||||
|  | import android.content.IntentFilter; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.text.TextUtils; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Settings; | ||||||
|  | import org.citra.citra_emu.features.settings.utils.SettingsFile; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryInitialization; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryStateReceiver; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | import org.citra.citra_emu.utils.ThemeUtil; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | 
 | ||||||
|  | public final class SettingsActivityPresenter { | ||||||
|  |     private static final String KEY_SHOULD_SAVE = "should_save"; | ||||||
|  | 
 | ||||||
|  |     private SettingsActivityView mView; | ||||||
|  | 
 | ||||||
|  |     private Settings mSettings = new Settings(); | ||||||
|  | 
 | ||||||
|  |     private boolean mShouldSave; | ||||||
|  | 
 | ||||||
|  |     private DirectoryStateReceiver directoryStateReceiver; | ||||||
|  | 
 | ||||||
|  |     private String menuTag; | ||||||
|  |     private String gameId; | ||||||
|  | 
 | ||||||
|  |     public SettingsActivityPresenter(SettingsActivityView view) { | ||||||
|  |         mView = view; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onCreate(Bundle savedInstanceState, String menuTag, String gameId) { | ||||||
|  |         if (savedInstanceState == null) { | ||||||
|  |             this.menuTag = menuTag; | ||||||
|  |             this.gameId = gameId; | ||||||
|  |         } else { | ||||||
|  |             mShouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onStart() { | ||||||
|  |         prepareCitraDirectoriesIfNeeded(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     void loadSettingsUI() { | ||||||
|  |         if (mSettings.isEmpty()) { | ||||||
|  |             if (!TextUtils.isEmpty(gameId)) { | ||||||
|  |                 mSettings.loadSettings(gameId, mView); | ||||||
|  |             } else { | ||||||
|  |                 mSettings.loadSettings(mView); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         mView.showSettingsFragment(menuTag, false, gameId); | ||||||
|  |         mView.onSettingsFileLoaded(mSettings); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void prepareCitraDirectoriesIfNeeded() { | ||||||
|  |         File configFile = new File(DirectoryInitialization.getUserDirectory() + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini"); | ||||||
|  |         if (!configFile.exists()) { | ||||||
|  |             Log.error("Citra config file could not be found!"); | ||||||
|  |         } | ||||||
|  |         if (DirectoryInitialization.areCitraDirectoriesReady()) { | ||||||
|  |             loadSettingsUI(); | ||||||
|  |         } else { | ||||||
|  |             mView.showLoading(); | ||||||
|  |             IntentFilter statusIntentFilter = new IntentFilter( | ||||||
|  |                     DirectoryInitialization.BROADCAST_ACTION); | ||||||
|  | 
 | ||||||
|  |             directoryStateReceiver = | ||||||
|  |                     new DirectoryStateReceiver(directoryInitializationState -> | ||||||
|  |                     { | ||||||
|  |                         if (directoryInitializationState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) { | ||||||
|  |                             mView.hideLoading(); | ||||||
|  |                             loadSettingsUI(); | ||||||
|  |                         } else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) { | ||||||
|  |                             mView.showPermissionNeededHint(); | ||||||
|  |                             mView.hideLoading(); | ||||||
|  |                         } else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { | ||||||
|  |                             mView.showExternalStorageNotMountedHint(); | ||||||
|  |                             mView.hideLoading(); | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  | 
 | ||||||
|  |             mView.startDirectoryInitializationService(directoryStateReceiver, statusIntentFilter); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setSettings(Settings settings) { | ||||||
|  |         mSettings = settings; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Settings getSettings() { | ||||||
|  |         return mSettings; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onStop(boolean finishing) { | ||||||
|  |         if (directoryStateReceiver != null) { | ||||||
|  |             mView.stopListeningToDirectoryInitializationService(directoryStateReceiver); | ||||||
|  |             directoryStateReceiver = null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (mSettings != null && finishing && mShouldSave) { | ||||||
|  |             Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI..."); | ||||||
|  |             mSettings.saveSettings(mView); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         ThemeUtil.applyTheme(); | ||||||
|  | 
 | ||||||
|  |         NativeLibrary.ReloadSettings(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onSettingChanged() { | ||||||
|  |         mShouldSave = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void saveState(Bundle outState) { | ||||||
|  |         outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,103 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui; | ||||||
|  | 
 | ||||||
|  | import android.content.IntentFilter; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.Settings; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryStateReceiver; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Abstraction for the Activity that manages SettingsFragments. | ||||||
|  |  */ | ||||||
|  | public interface SettingsActivityView { | ||||||
|  |     /** | ||||||
|  |      * Show a new SettingsFragment. | ||||||
|  |      * | ||||||
|  |      * @param menuTag    Identifier for the settings group that should be displayed. | ||||||
|  |      * @param addToStack Whether or not this fragment should replace a previous one. | ||||||
|  |      */ | ||||||
|  |     void showSettingsFragment(String menuTag, boolean addToStack, String gameId); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called by a contained Fragment to get access to the Setting HashMap | ||||||
|  |      * loaded from disk, so that each Fragment doesn't need to perform its own | ||||||
|  |      * read operation. | ||||||
|  |      * | ||||||
|  |      * @return A possibly null HashMap of Settings. | ||||||
|  |      */ | ||||||
|  |     Settings getSettings(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Used to provide the Activity with Settings HashMaps if a Fragment already | ||||||
|  |      * has one; for example, if a rotation occurs, the Fragment will not be killed, | ||||||
|  |      * but the Activity will, so the Activity needs to have its HashMaps resupplied. | ||||||
|  |      * | ||||||
|  |      * @param settings The ArrayList of all the Settings HashMaps. | ||||||
|  |      */ | ||||||
|  |     void setSettings(Settings settings); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called when an asynchronous load operation completes. | ||||||
|  |      * | ||||||
|  |      * @param settings The (possibly null) result of the ini load operation. | ||||||
|  |      */ | ||||||
|  |     void onSettingsFileLoaded(Settings settings); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called when an asynchronous load operation fails. | ||||||
|  |      */ | ||||||
|  |     void onSettingsFileNotFound(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Display a popup text message on screen. | ||||||
|  |      * | ||||||
|  |      * @param message The contents of the onscreen message. | ||||||
|  |      * @param is_long Whether this should be a long Toast or short one. | ||||||
|  |      */ | ||||||
|  |     void showToastMessage(String message, boolean is_long); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * End the activity. | ||||||
|  |      */ | ||||||
|  |     void finish(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called by a containing Fragment to tell the Activity that a setting was changed; | ||||||
|  |      * unless this has been called, the Activity will not save to disk. | ||||||
|  |      */ | ||||||
|  |     void onSettingChanged(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show loading dialog while loading the settings | ||||||
|  |      */ | ||||||
|  |     void showLoading(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Hide the loading the dialog | ||||||
|  |      */ | ||||||
|  |     void hideLoading(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show a hint to the user that the app needs write to external storage access | ||||||
|  |      */ | ||||||
|  |     void showPermissionNeededHint(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show a hint to the user that the app needs the external storage to be mounted | ||||||
|  |      */ | ||||||
|  |     void showExternalStorageNotMountedHint(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Start the DirectoryInitialization and listen for the result. | ||||||
|  |      * | ||||||
|  |      * @param receiver the broadcast receiver for the DirectoryInitialization | ||||||
|  |      * @param filter   the Intent broadcasts to be received. | ||||||
|  |      */ | ||||||
|  |     void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Stop listening to the DirectoryInitialization. | ||||||
|  |      * | ||||||
|  |      * @param receiver The broadcast receiver to unregister. | ||||||
|  |      */ | ||||||
|  |     void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,487 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.DialogInterface; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.widget.DatePicker; | ||||||
|  | import android.widget.SeekBar; | ||||||
|  | import android.widget.TextView; | ||||||
|  | import android.widget.TimePicker; | ||||||
|  | 
 | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.dialogs.MotionAlertDialog; | ||||||
|  | import org.citra.citra_emu.features.settings.model.FloatSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.IntSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.StringSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SliderSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHolder; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.viewholder.PremiumViewHolder; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder; | ||||||
|  | import org.citra.citra_emu.ui.main.MainActivity; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | 
 | ||||||
|  | public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder> | ||||||
|  |         implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener { | ||||||
|  |     private SettingsFragmentView mView; | ||||||
|  |     private Context mContext; | ||||||
|  |     private ArrayList<SettingsItem> mSettings; | ||||||
|  | 
 | ||||||
|  |     private SettingsItem mClickedItem; | ||||||
|  |     private int mClickedPosition; | ||||||
|  |     private int mSeekbarProgress; | ||||||
|  | 
 | ||||||
|  |     private AlertDialog mDialog; | ||||||
|  |     private TextView mTextSliderValue; | ||||||
|  | 
 | ||||||
|  |     public SettingsAdapter(SettingsFragmentView view, Context context) { | ||||||
|  |         mView = view; | ||||||
|  |         mContext = context; | ||||||
|  |         mClickedPosition = -1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { | ||||||
|  |         View view; | ||||||
|  |         LayoutInflater inflater = LayoutInflater.from(parent.getContext()); | ||||||
|  | 
 | ||||||
|  |         switch (viewType) { | ||||||
|  |             case SettingsItem.TYPE_HEADER: | ||||||
|  |                 view = inflater.inflate(R.layout.list_item_settings_header, parent, false); | ||||||
|  |                 return new HeaderViewHolder(view, this); | ||||||
|  | 
 | ||||||
|  |             case SettingsItem.TYPE_CHECKBOX: | ||||||
|  |                 view = inflater.inflate(R.layout.list_item_setting_checkbox, parent, false); | ||||||
|  |                 return new CheckBoxSettingViewHolder(view, this); | ||||||
|  | 
 | ||||||
|  |             case SettingsItem.TYPE_SINGLE_CHOICE: | ||||||
|  |             case SettingsItem.TYPE_STRING_SINGLE_CHOICE: | ||||||
|  |                 view = inflater.inflate(R.layout.list_item_setting, parent, false); | ||||||
|  |                 return new SingleChoiceViewHolder(view, this); | ||||||
|  | 
 | ||||||
|  |             case SettingsItem.TYPE_SLIDER: | ||||||
|  |                 view = inflater.inflate(R.layout.list_item_setting, parent, false); | ||||||
|  |                 return new SliderViewHolder(view, this); | ||||||
|  | 
 | ||||||
|  |             case SettingsItem.TYPE_SUBMENU: | ||||||
|  |                 view = inflater.inflate(R.layout.list_item_setting, parent, false); | ||||||
|  |                 return new SubmenuViewHolder(view, this); | ||||||
|  | 
 | ||||||
|  |             case SettingsItem.TYPE_INPUT_BINDING: | ||||||
|  |                 view = inflater.inflate(R.layout.list_item_setting, parent, false); | ||||||
|  |                 return new InputBindingSettingViewHolder(view, this, mContext); | ||||||
|  | 
 | ||||||
|  |             case SettingsItem.TYPE_DATETIME_SETTING: | ||||||
|  |                 view = inflater.inflate(R.layout.list_item_setting, parent, false); | ||||||
|  |                 return new DateTimeViewHolder(view, this); | ||||||
|  | 
 | ||||||
|  |             case SettingsItem.TYPE_PREMIUM: | ||||||
|  |                 view = inflater.inflate(R.layout.premium_item_setting, parent, false); | ||||||
|  |                 return new PremiumViewHolder(view, this, mView); | ||||||
|  | 
 | ||||||
|  |             default: | ||||||
|  |                 Log.error("[SettingsAdapter] Invalid view type: " + viewType); | ||||||
|  |                 return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onBindViewHolder(SettingViewHolder holder, int position) { | ||||||
|  |         holder.bind(getItem(position)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private SettingsItem getItem(int position) { | ||||||
|  |         return mSettings.get(position); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getItemCount() { | ||||||
|  |         if (mSettings != null) { | ||||||
|  |             return mSettings.size(); | ||||||
|  |         } else { | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getItemViewType(int position) { | ||||||
|  |         return getItem(position).getType(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setSettings(ArrayList<SettingsItem> settings) { | ||||||
|  |         mSettings = settings; | ||||||
|  |         notifyDataSetChanged(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) { | ||||||
|  |         IntSetting setting = item.setChecked(checked); | ||||||
|  |         notifyItemChanged(position); | ||||||
|  | 
 | ||||||
|  |         if (setting != null) { | ||||||
|  |             mView.putSetting(setting); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         mView.onSettingChanged(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onSingleChoiceClick(PremiumSingleChoiceSetting item) { | ||||||
|  |         mClickedItem = item; | ||||||
|  | 
 | ||||||
|  |         int value = getSelectionForSingleChoiceValue(item); | ||||||
|  | 
 | ||||||
|  |         AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); | ||||||
|  | 
 | ||||||
|  |         builder.setTitle(item.getNameId()); | ||||||
|  |         builder.setSingleChoiceItems(item.getChoicesId(), value, this); | ||||||
|  | 
 | ||||||
|  |         mDialog = builder.show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onSingleChoiceClick(SingleChoiceSetting item) { | ||||||
|  |         mClickedItem = item; | ||||||
|  | 
 | ||||||
|  |         int value = getSelectionForSingleChoiceValue(item); | ||||||
|  | 
 | ||||||
|  |         AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); | ||||||
|  | 
 | ||||||
|  |         builder.setTitle(item.getNameId()); | ||||||
|  |         builder.setSingleChoiceItems(item.getChoicesId(), value, this); | ||||||
|  | 
 | ||||||
|  |         mDialog = builder.show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onSingleChoiceClick(SingleChoiceSetting item, int position) { | ||||||
|  |         mClickedPosition = position; | ||||||
|  | 
 | ||||||
|  |         if (!item.isPremium() || MainActivity.isPremiumActive()) { | ||||||
|  |             // Setting is either not Premium, or the user has Premium | ||||||
|  |             onSingleChoiceClick(item); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // User needs Premium, invoke the billing flow | ||||||
|  |         MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onSingleChoiceClick(PremiumSingleChoiceSetting item, int position) { | ||||||
|  |         mClickedPosition = position; | ||||||
|  | 
 | ||||||
|  |         if (!item.isPremium() || MainActivity.isPremiumActive()) { | ||||||
|  |             // Setting is either not Premium, or the user has Premium | ||||||
|  |             onSingleChoiceClick(item); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // User needs Premium, invoke the billing flow | ||||||
|  |         MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onStringSingleChoiceClick(StringSingleChoiceSetting item) { | ||||||
|  |         mClickedItem = item; | ||||||
|  | 
 | ||||||
|  |         AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); | ||||||
|  | 
 | ||||||
|  |         builder.setTitle(item.getNameId()); | ||||||
|  |         builder.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this); | ||||||
|  | 
 | ||||||
|  |         mDialog = builder.show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) { | ||||||
|  |         mClickedPosition = position; | ||||||
|  | 
 | ||||||
|  |         if (!item.isPremium() || MainActivity.isPremiumActive()) { | ||||||
|  |             // Setting is either not Premium, or the user has Premium | ||||||
|  |             onStringSingleChoiceClick(item); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // User needs Premium, invoke the billing flow | ||||||
|  |         MainActivity.invokePremiumBilling(() -> onStringSingleChoiceClick(item)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog(); | ||||||
|  | 
 | ||||||
|  |     public void onDateTimeClick(DateTimeSetting item, int position) { | ||||||
|  |         mClickedItem = item; | ||||||
|  |         mClickedPosition = position; | ||||||
|  | 
 | ||||||
|  |         AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); | ||||||
|  | 
 | ||||||
|  |         LayoutInflater inflater = LayoutInflater.from(mView.getActivity()); | ||||||
|  |         View view = inflater.inflate(R.layout.sysclock_datetime_picker, null); | ||||||
|  | 
 | ||||||
|  |         DatePicker dp = view.findViewById(R.id.date_picker); | ||||||
|  |         TimePicker tp = view.findViewById(R.id.time_picker); | ||||||
|  | 
 | ||||||
|  |         //set date and time to substrings of settingValue; format = 2018-12-24 04:20:69 (alright maybe not that 69) | ||||||
|  |         String settingValue = item.getValue(); | ||||||
|  |         dp.updateDate(Integer.parseInt(settingValue.substring(0, 4)), Integer.parseInt(settingValue.substring(5, 7)) - 1, Integer.parseInt(settingValue.substring(8, 10))); | ||||||
|  | 
 | ||||||
|  |         tp.setIs24HourView(true); | ||||||
|  |         tp.setHour(Integer.parseInt(settingValue.substring(11, 13))); | ||||||
|  |         tp.setMinute(Integer.parseInt(settingValue.substring(14, 16))); | ||||||
|  | 
 | ||||||
|  |         DialogInterface.OnClickListener ok = (dialog, which) -> { | ||||||
|  |             //set it | ||||||
|  |             int year = dp.getYear(); | ||||||
|  |             if (year < 2000) { | ||||||
|  |                 year = 2000; | ||||||
|  |             } | ||||||
|  |             String month = ("00" + (dp.getMonth() + 1)).substring(String.valueOf(dp.getMonth() + 1).length()); | ||||||
|  |             String day = ("00" + dp.getDayOfMonth()).substring(String.valueOf(dp.getDayOfMonth()).length()); | ||||||
|  |             String hr = ("00" + tp.getHour()).substring(String.valueOf(tp.getHour()).length()); | ||||||
|  |             String min = ("00" + tp.getMinute()).substring(String.valueOf(tp.getMinute()).length()); | ||||||
|  |             String datetime = year + "-" + month + "-" + day + " " + hr + ":" + min + ":01"; | ||||||
|  | 
 | ||||||
|  |             StringSetting setting = item.setSelectedValue(datetime); | ||||||
|  |             if (setting != null) { | ||||||
|  |                 mView.putSetting(setting); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             mView.onSettingChanged(); | ||||||
|  | 
 | ||||||
|  |             mClickedItem = null; | ||||||
|  |             closeDialog(); | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         builder.setView(view); | ||||||
|  |         builder.setPositiveButton(android.R.string.ok, ok); | ||||||
|  |         builder.setNegativeButton(android.R.string.cancel, defaultCancelListener); | ||||||
|  |         mDialog = builder.show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onSliderClick(SliderSetting item, int position) { | ||||||
|  |         mClickedItem = item; | ||||||
|  |         mClickedPosition = position; | ||||||
|  |         mSeekbarProgress = item.getSelectedValue(); | ||||||
|  |         AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); | ||||||
|  | 
 | ||||||
|  |         LayoutInflater inflater = LayoutInflater.from(mView.getActivity()); | ||||||
|  |         View view = inflater.inflate(R.layout.dialog_seekbar, null); | ||||||
|  | 
 | ||||||
|  |         SeekBar seekbar = view.findViewById(R.id.seekbar); | ||||||
|  | 
 | ||||||
|  |         builder.setTitle(item.getNameId()); | ||||||
|  |         builder.setView(view); | ||||||
|  |         builder.setPositiveButton(android.R.string.ok, this); | ||||||
|  |         builder.setNegativeButton(android.R.string.cancel, defaultCancelListener); | ||||||
|  |         builder.setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> { | ||||||
|  |             seekbar.setProgress(item.getDefaultValue()); | ||||||
|  |             onClick(dialog, which); | ||||||
|  |         }); | ||||||
|  |         mDialog = builder.show(); | ||||||
|  | 
 | ||||||
|  |         mTextSliderValue = view.findViewById(R.id.text_value); | ||||||
|  |         mTextSliderValue.setText(String.valueOf(mSeekbarProgress)); | ||||||
|  | 
 | ||||||
|  |         TextView units = view.findViewById(R.id.text_units); | ||||||
|  |         units.setText(item.getUnits()); | ||||||
|  | 
 | ||||||
|  |         seekbar.setMin(item.getMin()); | ||||||
|  |         seekbar.setMax(item.getMax()); | ||||||
|  |         seekbar.setProgress(mSeekbarProgress); | ||||||
|  | 
 | ||||||
|  |         seekbar.setOnSeekBarChangeListener(this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onSubmenuClick(SubmenuSetting item) { | ||||||
|  |         mView.loadSubMenu(item.getMenuKey()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onInputBindingClick(final InputBindingSetting item, final int position) { | ||||||
|  |         final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item); | ||||||
|  |         dialog.setTitle(R.string.input_binding); | ||||||
|  | 
 | ||||||
|  |         int messageResId = R.string.input_binding_description; | ||||||
|  |         if (item.IsAxisMappingSupported() && !item.IsTrigger()) { | ||||||
|  |             // Use specialized message for axis left/right or up/down | ||||||
|  |             if (item.IsHorizontalOrientation()) { | ||||||
|  |                 messageResId = R.string.input_binding_description_horizontal_axis; | ||||||
|  |             } else { | ||||||
|  |                 messageResId = R.string.input_binding_description_vertical_axis; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId()))); | ||||||
|  |         dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), this); | ||||||
|  |         dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) -> | ||||||
|  |                 item.removeOldMapping()); | ||||||
|  |         dialog.setOnDismissListener(dialog1 -> | ||||||
|  |         { | ||||||
|  |             StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getValue()); | ||||||
|  |             notifyItemChanged(position); | ||||||
|  | 
 | ||||||
|  |             mView.putSetting(setting); | ||||||
|  | 
 | ||||||
|  |             mView.onSettingChanged(); | ||||||
|  |         }); | ||||||
|  |         dialog.setCanceledOnTouchOutside(false); | ||||||
|  |         dialog.show(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onClick(DialogInterface dialog, int which) { | ||||||
|  |         if (mClickedItem instanceof SingleChoiceSetting) { | ||||||
|  |             SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem; | ||||||
|  | 
 | ||||||
|  |             int value = getValueForSingleChoiceSelection(scSetting, which); | ||||||
|  |             if (scSetting.getSelectedValue() != value) { | ||||||
|  |                 mView.onSettingChanged(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Get the backing Setting, which may be null (if for example it was missing from the file) | ||||||
|  |             IntSetting setting = scSetting.setSelectedValue(value); | ||||||
|  |             if (setting != null) { | ||||||
|  |                 mView.putSetting(setting); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             closeDialog(); | ||||||
|  |         } else if (mClickedItem instanceof PremiumSingleChoiceSetting) { | ||||||
|  |             PremiumSingleChoiceSetting scSetting = (PremiumSingleChoiceSetting) mClickedItem; | ||||||
|  |             scSetting.setSelectedValue(getValueForSingleChoiceSelection(scSetting, which)); | ||||||
|  |             closeDialog(); | ||||||
|  |         } else if (mClickedItem instanceof StringSingleChoiceSetting) { | ||||||
|  |             StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem; | ||||||
|  |             String value = scSetting.getValueAt(which); | ||||||
|  |             if (!scSetting.getSelectedValue().equals(value)) | ||||||
|  |                 mView.onSettingChanged(); | ||||||
|  | 
 | ||||||
|  |             StringSetting setting = scSetting.setSelectedValue(value); | ||||||
|  |             if (setting != null) { | ||||||
|  |                 mView.putSetting(setting); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             closeDialog(); | ||||||
|  |         } else if (mClickedItem instanceof SliderSetting) { | ||||||
|  |             SliderSetting sliderSetting = (SliderSetting) mClickedItem; | ||||||
|  |             if (sliderSetting.getSelectedValue() != mSeekbarProgress) { | ||||||
|  |                 mView.onSettingChanged(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (sliderSetting.getSetting() instanceof FloatSetting) { | ||||||
|  |                 float value = (float) mSeekbarProgress; | ||||||
|  | 
 | ||||||
|  |                 FloatSetting setting = sliderSetting.setSelectedValue(value); | ||||||
|  |                 if (setting != null) { | ||||||
|  |                     mView.putSetting(setting); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 IntSetting setting = sliderSetting.setSelectedValue(mSeekbarProgress); | ||||||
|  |                 if (setting != null) { | ||||||
|  |                     mView.putSetting(setting); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             closeDialog(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         mClickedItem = null; | ||||||
|  |         mSeekbarProgress = -1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void closeDialog() { | ||||||
|  |         if (mDialog != null) { | ||||||
|  |             if (mClickedPosition != -1) { | ||||||
|  |                 notifyItemChanged(mClickedPosition); | ||||||
|  |                 mClickedPosition = -1; | ||||||
|  |             } | ||||||
|  |             mDialog.dismiss(); | ||||||
|  |             mDialog = null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { | ||||||
|  |         mSeekbarProgress = progress; | ||||||
|  |         mTextSliderValue.setText(String.valueOf(mSeekbarProgress)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onStartTrackingTouch(SeekBar seekBar) { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onStopTrackingTouch(SeekBar seekBar) { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) { | ||||||
|  |         int valuesId = item.getValuesId(); | ||||||
|  | 
 | ||||||
|  |         if (valuesId > 0) { | ||||||
|  |             int[] valuesArray = mContext.getResources().getIntArray(valuesId); | ||||||
|  |             return valuesArray[which]; | ||||||
|  |         } else { | ||||||
|  |             return which; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private int getValueForSingleChoiceSelection(PremiumSingleChoiceSetting item, int which) { | ||||||
|  |         int valuesId = item.getValuesId(); | ||||||
|  | 
 | ||||||
|  |         if (valuesId > 0) { | ||||||
|  |             int[] valuesArray = mContext.getResources().getIntArray(valuesId); | ||||||
|  |             return valuesArray[which]; | ||||||
|  |         } else { | ||||||
|  |             return which; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) { | ||||||
|  |         int value = item.getSelectedValue(); | ||||||
|  |         int valuesId = item.getValuesId(); | ||||||
|  | 
 | ||||||
|  |         if (valuesId > 0) { | ||||||
|  |             int[] valuesArray = mContext.getResources().getIntArray(valuesId); | ||||||
|  |             for (int index = 0; index < valuesArray.length; index++) { | ||||||
|  |                 int current = valuesArray[index]; | ||||||
|  |                 if (current == value) { | ||||||
|  |                     return index; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             return value; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return -1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private int getSelectionForSingleChoiceValue(PremiumSingleChoiceSetting item) { | ||||||
|  |         int value = item.getSelectedValue(); | ||||||
|  |         int valuesId = item.getValuesId(); | ||||||
|  | 
 | ||||||
|  |         if (valuesId > 0) { | ||||||
|  |             int[] valuesArray = mContext.getResources().getIntArray(valuesId); | ||||||
|  |             for (int index = 0; index < valuesArray.length; index++) { | ||||||
|  |                 int current = valuesArray[index]; | ||||||
|  |                 if (current == value) { | ||||||
|  |                     return index; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             return value; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return -1; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,136 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.fragment.app.Fragment; | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Settings; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.ui.DividerItemDecoration; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | 
 | ||||||
|  | public final class SettingsFragment extends Fragment implements SettingsFragmentView { | ||||||
|  |     private static final String ARGUMENT_MENU_TAG = "menu_tag"; | ||||||
|  |     private static final String ARGUMENT_GAME_ID = "game_id"; | ||||||
|  | 
 | ||||||
|  |     private SettingsFragmentPresenter mPresenter = new SettingsFragmentPresenter(this); | ||||||
|  |     private SettingsActivityView mActivity; | ||||||
|  | 
 | ||||||
|  |     private SettingsAdapter mAdapter; | ||||||
|  | 
 | ||||||
|  |     public static Fragment newInstance(String menuTag, String gameId) { | ||||||
|  |         SettingsFragment fragment = new SettingsFragment(); | ||||||
|  | 
 | ||||||
|  |         Bundle arguments = new Bundle(); | ||||||
|  |         arguments.putString(ARGUMENT_MENU_TAG, menuTag); | ||||||
|  |         arguments.putString(ARGUMENT_GAME_ID, gameId); | ||||||
|  | 
 | ||||||
|  |         fragment.setArguments(arguments); | ||||||
|  |         return fragment; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onAttach(@NonNull Context context) { | ||||||
|  |         super.onAttach(context); | ||||||
|  | 
 | ||||||
|  |         mActivity = (SettingsActivityView) context; | ||||||
|  |         mPresenter.onAttach(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  | 
 | ||||||
|  |         setRetainInstance(true); | ||||||
|  |         String menuTag = getArguments().getString(ARGUMENT_MENU_TAG); | ||||||
|  |         String gameId = getArguments().getString(ARGUMENT_GAME_ID); | ||||||
|  | 
 | ||||||
|  |         mAdapter = new SettingsAdapter(this, getActivity()); | ||||||
|  | 
 | ||||||
|  |         mPresenter.onCreate(menuTag, gameId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { | ||||||
|  |         return inflater.inflate(R.layout.fragment_settings, container, false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { | ||||||
|  |         LinearLayoutManager manager = new LinearLayoutManager(getActivity()); | ||||||
|  | 
 | ||||||
|  |         RecyclerView recyclerView = view.findViewById(R.id.list_settings); | ||||||
|  | 
 | ||||||
|  |         recyclerView.setAdapter(mAdapter); | ||||||
|  |         recyclerView.setLayoutManager(manager); | ||||||
|  |         recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null)); | ||||||
|  | 
 | ||||||
|  |         SettingsActivityView activity = (SettingsActivityView) getActivity(); | ||||||
|  | 
 | ||||||
|  |         mPresenter.onViewCreated(activity.getSettings()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onDetach() { | ||||||
|  |         super.onDetach(); | ||||||
|  |         mActivity = null; | ||||||
|  | 
 | ||||||
|  |         if (mAdapter != null) { | ||||||
|  |             mAdapter.closeDialog(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onSettingsFileLoaded(Settings settings) { | ||||||
|  |         mPresenter.setSettings(settings); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void passSettingsToActivity(Settings settings) { | ||||||
|  |         if (mActivity != null) { | ||||||
|  |             mActivity.setSettings(settings); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void showSettingsList(ArrayList<SettingsItem> settingsList) { | ||||||
|  |         mAdapter.setSettings(settingsList); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void loadDefaultSettings() { | ||||||
|  |         mPresenter.loadDefaultSettings(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void loadSubMenu(String menuKey) { | ||||||
|  |         mActivity.showSettingsFragment(menuKey, true, getArguments().getString(ARGUMENT_GAME_ID)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void showToastMessage(String message, boolean is_long) { | ||||||
|  |         mActivity.showToastMessage(message, is_long); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void putSetting(Setting setting) { | ||||||
|  |         mPresenter.putSetting(setting); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onSettingChanged() { | ||||||
|  |         mActivity.onSettingChanged(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,409 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui; | ||||||
|  | 
 | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.hardware.camera2.CameraAccessException; | ||||||
|  | import android.hardware.camera2.CameraCharacteristics; | ||||||
|  | import android.hardware.camera2.CameraManager; | ||||||
|  | import android.text.TextUtils; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.SettingSection; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Settings; | ||||||
|  | import org.citra.citra_emu.features.settings.model.StringSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.HeaderSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.PremiumHeader; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SliderSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.utils.SettingsFile; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Objects; | ||||||
|  | 
 | ||||||
|  | public final class SettingsFragmentPresenter { | ||||||
|  |     private SettingsFragmentView mView; | ||||||
|  | 
 | ||||||
|  |     private String mMenuTag; | ||||||
|  |     private String mGameID; | ||||||
|  | 
 | ||||||
|  |     private Settings mSettings; | ||||||
|  |     private ArrayList<SettingsItem> mSettingsList; | ||||||
|  | 
 | ||||||
|  |     public SettingsFragmentPresenter(SettingsFragmentView view) { | ||||||
|  |         mView = view; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onCreate(String menuTag, String gameId) { | ||||||
|  |         mGameID = gameId; | ||||||
|  |         mMenuTag = menuTag; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onViewCreated(Settings settings) { | ||||||
|  |         setSettings(settings); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * If the screen is rotated, the Activity will forget the settings map. This fragment | ||||||
|  |      * won't, though; so rather than have the Activity reload from disk, have the fragment pass | ||||||
|  |      * the settings map back to the Activity. | ||||||
|  |      */ | ||||||
|  |     public void onAttach() { | ||||||
|  |         if (mSettings != null) { | ||||||
|  |             mView.passSettingsToActivity(mSettings); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void putSetting(Setting setting) { | ||||||
|  |         mSettings.getSection(setting.getSection()).putSetting(setting); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private StringSetting asStringSetting(Setting setting) { | ||||||
|  |         if (setting == null) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         StringSetting stringSetting = new StringSetting(setting.getKey(), setting.getSection(), setting.getValueAsString()); | ||||||
|  |         putSetting(stringSetting); | ||||||
|  |         return stringSetting; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void loadDefaultSettings() { | ||||||
|  |         loadSettingsList(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setSettings(Settings settings) { | ||||||
|  |         if (mSettingsList == null && settings != null) { | ||||||
|  |             mSettings = settings; | ||||||
|  | 
 | ||||||
|  |             loadSettingsList(); | ||||||
|  |         } else { | ||||||
|  |             mView.getActivity().setTitle(R.string.preferences_settings); | ||||||
|  |             mView.showSettingsList(mSettingsList); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void loadSettingsList() { | ||||||
|  |         if (!TextUtils.isEmpty(mGameID)) { | ||||||
|  |             mView.getActivity().setTitle("Game Settings: " + mGameID); | ||||||
|  |         } | ||||||
|  |         ArrayList<SettingsItem> sl = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  |         if (mMenuTag == null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         switch (mMenuTag) { | ||||||
|  |             case SettingsFile.FILE_NAME_CONFIG: | ||||||
|  |                 addConfigSettings(sl); | ||||||
|  |                 break; | ||||||
|  |             case Settings.SECTION_PREMIUM: | ||||||
|  |                 addPremiumSettings(sl); | ||||||
|  |                 break; | ||||||
|  |             case Settings.SECTION_CORE: | ||||||
|  |                 addGeneralSettings(sl); | ||||||
|  |                 break; | ||||||
|  |             case Settings.SECTION_SYSTEM: | ||||||
|  |                 addSystemSettings(sl); | ||||||
|  |                 break; | ||||||
|  |             case Settings.SECTION_CAMERA: | ||||||
|  |                 addCameraSettings(sl); | ||||||
|  |                 break; | ||||||
|  |             case Settings.SECTION_CONTROLS: | ||||||
|  |                 addInputSettings(sl); | ||||||
|  |                 break; | ||||||
|  |             case Settings.SECTION_RENDERER: | ||||||
|  |                 addGraphicsSettings(sl); | ||||||
|  |                 break; | ||||||
|  |             case Settings.SECTION_AUDIO: | ||||||
|  |                 addAudioSettings(sl); | ||||||
|  |                 break; | ||||||
|  |             case Settings.SECTION_DEBUG: | ||||||
|  |                 addDebugSettings(sl); | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 mView.showToastMessage("Unimplemented menu", false); | ||||||
|  |                 return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         mSettingsList = sl; | ||||||
|  |         mView.showSettingsList(mSettingsList); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addConfigSettings(ArrayList<SettingsItem> sl) { | ||||||
|  |         mView.getActivity().setTitle(R.string.preferences_settings); | ||||||
|  | 
 | ||||||
|  |         sl.add(new SubmenuSetting(null, null, R.string.preferences_premium, 0, Settings.SECTION_PREMIUM)); | ||||||
|  |         sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE)); | ||||||
|  |         sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM)); | ||||||
|  |         sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA)); | ||||||
|  |         sl.add(new SubmenuSetting(null, null, R.string.preferences_controls, 0, Settings.SECTION_CONTROLS)); | ||||||
|  |         sl.add(new SubmenuSetting(null, null, R.string.preferences_graphics, 0, Settings.SECTION_RENDERER)); | ||||||
|  |         sl.add(new SubmenuSetting(null, null, R.string.preferences_audio, 0, Settings.SECTION_AUDIO)); | ||||||
|  |         sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addPremiumSettings(ArrayList<SettingsItem> sl) { | ||||||
|  |         mView.getActivity().setTitle(R.string.preferences_premium); | ||||||
|  | 
 | ||||||
|  |         SettingSection premiumSection = mSettings.getSection(Settings.SECTION_PREMIUM); | ||||||
|  |         Setting design = premiumSection.getSetting(SettingsFile.KEY_DESIGN); | ||||||
|  | 
 | ||||||
|  |         sl.add(new PremiumHeader()); | ||||||
|  | 
 | ||||||
|  |         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { | ||||||
|  |             sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNames, R.array.designValues, 0, design, mView)); | ||||||
|  |         } else { | ||||||
|  |             // Pre-Android 10 does not support System Default | ||||||
|  |             sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNamesOld, R.array.designValuesOld, 0, design, mView)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         String[] textureFilterNames = NativeLibrary.GetTextureFilterNames(); | ||||||
|  |         Setting textureFilterName = premiumSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME); | ||||||
|  |         sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME, Settings.SECTION_PREMIUM, R.string.texture_filter_name, R.string.texture_filter_description, textureFilterNames, textureFilterNames, "none", textureFilterName)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addGeneralSettings(ArrayList<SettingsItem> sl) { | ||||||
|  |         mView.getActivity().setTitle(R.string.preferences_general); | ||||||
|  | 
 | ||||||
|  |         SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); | ||||||
|  |         Setting frameLimitEnable = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED); | ||||||
|  |         Setting frameLimitValue = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT); | ||||||
|  | 
 | ||||||
|  |         sl.add(new CheckBoxSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED, Settings.SECTION_RENDERER, R.string.frame_limit_enable, R.string.frame_limit_enable_description, true, frameLimitEnable)); | ||||||
|  |         sl.add(new SliderSetting(SettingsFile.KEY_FRAME_LIMIT, Settings.SECTION_RENDERER, R.string.frame_limit_slider, R.string.frame_limit_slider_description, 1, 200, "%", 100, frameLimitValue)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addSystemSettings(ArrayList<SettingsItem> sl) { | ||||||
|  |         mView.getActivity().setTitle(R.string.preferences_system); | ||||||
|  | 
 | ||||||
|  |         SettingSection systemSection = mSettings.getSection(Settings.SECTION_SYSTEM); | ||||||
|  |         Setting region = systemSection.getSetting(SettingsFile.KEY_REGION_VALUE); | ||||||
|  |         Setting language = systemSection.getSetting(SettingsFile.KEY_LANGUAGE); | ||||||
|  |         Setting systemClock = systemSection.getSetting(SettingsFile.KEY_INIT_CLOCK); | ||||||
|  |         Setting dateTime = systemSection.getSetting(SettingsFile.KEY_INIT_TIME); | ||||||
|  | 
 | ||||||
|  |         sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, Settings.SECTION_SYSTEM, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, -1, region)); | ||||||
|  |         sl.add(new SingleChoiceSetting(SettingsFile.KEY_LANGUAGE, Settings.SECTION_SYSTEM, R.string.emulated_language, 0, R.array.languageNames, R.array.languageValues, 1, language)); | ||||||
|  |         sl.add(new SingleChoiceSetting(SettingsFile.KEY_INIT_CLOCK, Settings.SECTION_SYSTEM, R.string.init_clock, R.string.init_clock_description, R.array.systemClockNames, R.array.systemClockValues, 0, systemClock)); | ||||||
|  |         sl.add(new DateTimeSetting(SettingsFile.KEY_INIT_TIME, Settings.SECTION_SYSTEM, R.string.init_time, R.string.init_time_description, "2000-01-01 00:00:01", dateTime)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addCameraSettings(ArrayList<SettingsItem> sl) { | ||||||
|  |         final Activity activity = mView.getActivity(); | ||||||
|  |         activity.setTitle(R.string.preferences_camera); | ||||||
|  | 
 | ||||||
|  |         // Get the camera IDs | ||||||
|  |         CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); | ||||||
|  |         ArrayList<String> supportedCameraNameList = new ArrayList<>(); | ||||||
|  |         ArrayList<String> supportedCameraIdList = new ArrayList<>(); | ||||||
|  |         if (cameraManager != null) { | ||||||
|  |             try { | ||||||
|  |                 for (String id : cameraManager.getCameraIdList()) { | ||||||
|  |                     final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); | ||||||
|  |                     if (Objects.requireNonNull(characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { | ||||||
|  |                         continue; // Legacy cameras cannot be used with the NDK | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     supportedCameraIdList.add(id); | ||||||
|  | 
 | ||||||
|  |                     final int facing = Objects.requireNonNull(characteristics.get(CameraCharacteristics.LENS_FACING)); | ||||||
|  |                     int stringId = R.string.camera_facing_external; | ||||||
|  |                     switch (facing) { | ||||||
|  |                         case CameraCharacteristics.LENS_FACING_FRONT: | ||||||
|  |                             stringId = R.string.camera_facing_front; | ||||||
|  |                             break; | ||||||
|  |                         case CameraCharacteristics.LENS_FACING_BACK: | ||||||
|  |                             stringId = R.string.camera_facing_back; | ||||||
|  |                             break; | ||||||
|  |                         case CameraCharacteristics.LENS_FACING_EXTERNAL: | ||||||
|  |                             stringId = R.string.camera_facing_external; | ||||||
|  |                             break; | ||||||
|  |                     } | ||||||
|  |                     supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(stringId))); | ||||||
|  |                 } | ||||||
|  |             } catch (CameraAccessException e) { | ||||||
|  |                 Log.error("Couldn't retrieve camera list"); | ||||||
|  |                 e.printStackTrace(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Create the names and values for display | ||||||
|  |         ArrayList<String> cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames))); | ||||||
|  |         cameraDeviceNameList.addAll(supportedCameraNameList); | ||||||
|  |         ArrayList<String> cameraDeviceValueList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceValues))); | ||||||
|  |         cameraDeviceValueList.addAll(supportedCameraIdList); | ||||||
|  | 
 | ||||||
|  |         final String[] cameraDeviceNames = cameraDeviceNameList.toArray(new String[]{}); | ||||||
|  |         final String[] cameraDeviceValues = cameraDeviceValueList.toArray(new String[]{}); | ||||||
|  | 
 | ||||||
|  |         final boolean haveCameraDevices = !supportedCameraIdList.isEmpty(); | ||||||
|  | 
 | ||||||
|  |         String[] imageSourceNames = activity.getResources().getStringArray(R.array.cameraImageSourceNames); | ||||||
|  |         String[] imageSourceValues = activity.getResources().getStringArray(R.array.cameraImageSourceValues); | ||||||
|  |         if (!haveCameraDevices) { | ||||||
|  |             // Remove the last entry (ndk / Device Camera) | ||||||
|  |             imageSourceNames = Arrays.copyOfRange(imageSourceNames, 0, imageSourceNames.length - 1); | ||||||
|  |             imageSourceValues = Arrays.copyOfRange(imageSourceValues, 0, imageSourceValues.length - 1); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final String defaultImageSource = haveCameraDevices ? "ndk" : "image"; | ||||||
|  | 
 | ||||||
|  |         SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA); | ||||||
|  | 
 | ||||||
|  |         Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME); | ||||||
|  |         Setting innerCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG)); | ||||||
|  |         Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP); | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0)); | ||||||
|  |         sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, innerCameraImageSource)); | ||||||
|  |         if (haveCameraDevices) | ||||||
|  |             sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_front", innerCameraConfig)); | ||||||
|  |         sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, innerCameraFlip)); | ||||||
|  | 
 | ||||||
|  |         Setting outerLeftCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME); | ||||||
|  |         Setting outerLeftCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG)); | ||||||
|  |         Setting outerLeftCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP); | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.outer_left_camera, 0)); | ||||||
|  |         sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerLeftCameraImageSource)); | ||||||
|  |         if (haveCameraDevices) | ||||||
|  |             sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerLeftCameraConfig)); | ||||||
|  |         sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerLeftCameraFlip)); | ||||||
|  | 
 | ||||||
|  |         Setting outerRightCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME); | ||||||
|  |         Setting outerRightCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG)); | ||||||
|  |         Setting outerRightCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP); | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.outer_right_camera, 0)); | ||||||
|  |         sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerRightCameraImageSource)); | ||||||
|  |         if (haveCameraDevices) | ||||||
|  |             sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerRightCameraConfig)); | ||||||
|  |         sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerRightCameraFlip)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addInputSettings(ArrayList<SettingsItem> sl) { | ||||||
|  |         mView.getActivity().setTitle(R.string.preferences_controls); | ||||||
|  | 
 | ||||||
|  |         SettingSection controlsSection = mSettings.getSection(Settings.SECTION_CONTROLS); | ||||||
|  |         Setting buttonA = controlsSection.getSetting(SettingsFile.KEY_BUTTON_A); | ||||||
|  |         Setting buttonB = controlsSection.getSetting(SettingsFile.KEY_BUTTON_B); | ||||||
|  |         Setting buttonX = controlsSection.getSetting(SettingsFile.KEY_BUTTON_X); | ||||||
|  |         Setting buttonY = controlsSection.getSetting(SettingsFile.KEY_BUTTON_Y); | ||||||
|  |         Setting buttonSelect = controlsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT); | ||||||
|  |         Setting buttonStart = controlsSection.getSetting(SettingsFile.KEY_BUTTON_START); | ||||||
|  |         Setting circlepadAxisVert = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL); | ||||||
|  |         Setting circlepadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL); | ||||||
|  |         Setting cstickAxisVert = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL); | ||||||
|  |         Setting cstickAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL); | ||||||
|  |         Setting dpadAxisVert = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL); | ||||||
|  |         Setting dpadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL); | ||||||
|  |         // Setting buttonUp = controlsSection.getSetting(SettingsFile.KEY_BUTTON_UP); | ||||||
|  |         // Setting buttonDown = controlsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN); | ||||||
|  |         // Setting buttonLeft = controlsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT); | ||||||
|  |         // Setting buttonRight = controlsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT); | ||||||
|  |         Setting buttonL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_L); | ||||||
|  |         Setting buttonR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_R); | ||||||
|  |         Setting buttonZL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZL); | ||||||
|  |         Setting buttonZR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZR); | ||||||
|  | 
 | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_CONTROLS, R.string.button_a, buttonA)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_CONTROLS, R.string.button_b, buttonB)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_CONTROLS, R.string.button_x, buttonX)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_CONTROLS, R.string.button_y, buttonY)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, Settings.SECTION_CONTROLS, R.string.button_select, buttonSelect)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_CONTROLS, R.string.button_start, buttonStart)); | ||||||
|  | 
 | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.controller_circlepad, 0)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, circlepadAxisVert)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, circlepadAxisHoriz)); | ||||||
|  | 
 | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.controller_c, 0)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, cstickAxisVert)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, cstickAxisHoriz)); | ||||||
|  | 
 | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, dpadAxisVert)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, dpadAxisHoriz)); | ||||||
|  | 
 | ||||||
|  |         // TODO(bunnei): Figure out what to do with these. Configuring is functional, but removing for MVP because they are confusing. | ||||||
|  |         // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_CONTROLS, R.string.generic_up, buttonUp)); | ||||||
|  |         // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_CONTROLS, R.string.generic_down, buttonDown)); | ||||||
|  |         // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_CONTROLS, R.string.generic_left, buttonLeft)); | ||||||
|  |         // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_CONTROLS, R.string.generic_right, buttonRight)); | ||||||
|  | 
 | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.controller_triggers, 0)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_CONTROLS, R.string.button_l, buttonL)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_CONTROLS, R.string.button_r, buttonR)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZL, Settings.SECTION_CONTROLS, R.string.button_zl, buttonZL)); | ||||||
|  |         sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZR, Settings.SECTION_CONTROLS, R.string.button_zr, buttonZR)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addGraphicsSettings(ArrayList<SettingsItem> sl) { | ||||||
|  |         mView.getActivity().setTitle(R.string.preferences_graphics); | ||||||
|  | 
 | ||||||
|  |         SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); | ||||||
|  |         Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR); | ||||||
|  |         Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE); | ||||||
|  |         Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL); | ||||||
|  |         Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D); | ||||||
|  |         Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D); | ||||||
|  |         Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE); | ||||||
|  | 
 | ||||||
|  |         SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT); | ||||||
|  |         Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE); | ||||||
|  |         Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT); | ||||||
|  |         Setting cardboardYShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT); | ||||||
|  | 
 | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.renderer, 0)); | ||||||
|  |         sl.add(new SliderSetting(SettingsFile.KEY_RESOLUTION_FACTOR, Settings.SECTION_RENDERER, R.string.internal_resolution, R.string.internal_resolution_description, 1, 4, "x", 1, resolutionFactor)); | ||||||
|  |         sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode)); | ||||||
|  |         sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, Settings.SECTION_RENDERER, R.string.shaders_accurate_mul, R.string.shaders_accurate_mul_description, false, shadersAccurateMul)); | ||||||
|  |         sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE, Settings.SECTION_RENDERER, R.string.use_disk_shader_cache, R.string.use_disk_shader_cache_description, true, useDiskShaderCache)); | ||||||
|  | 
 | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0)); | ||||||
|  |         sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode)); | ||||||
|  |         sl.add(new SliderSetting(SettingsFile.KEY_FACTOR_3D, Settings.SECTION_RENDERER, R.string.factor3d, R.string.factor3d_description, 0, 100, "%", 0, factor3d)); | ||||||
|  | 
 | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.cardboard_vr, 0)); | ||||||
|  |         sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE, Settings.SECTION_LAYOUT, R.string.cardboard_screen_size, R.string.cardboard_screen_size_description, 30, 100, "%", 85, cardboardScreenSize)); | ||||||
|  |         sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_x_shift, R.string.cardboard_x_shift_description, -100, 100, "%", 0, cardboardXShift)); | ||||||
|  |         sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_y_shift, R.string.cardboard_y_shift_description, -100, 100, "%", 0, cardboardYShift)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addAudioSettings(ArrayList<SettingsItem> sl) { | ||||||
|  |         mView.getActivity().setTitle(R.string.preferences_audio); | ||||||
|  | 
 | ||||||
|  |         SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO); | ||||||
|  |         Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING); | ||||||
|  |         Setting micInputType = audioSection.getSetting(SettingsFile.KEY_MIC_INPUT_TYPE); | ||||||
|  | 
 | ||||||
|  |         sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, true, audioStretch)); | ||||||
|  |         sl.add(new SingleChoiceSetting(SettingsFile.KEY_MIC_INPUT_TYPE, Settings.SECTION_AUDIO, R.string.audio_input_type, 0, R.array.audioInputTypeNames, R.array.audioInputTypeValues, 1, micInputType)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addDebugSettings(ArrayList<SettingsItem> sl) { | ||||||
|  |         mView.getActivity().setTitle(R.string.preferences_debug); | ||||||
|  | 
 | ||||||
|  |         SettingSection coreSection = mSettings.getSection(Settings.SECTION_CORE); | ||||||
|  |         SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); | ||||||
|  |         Setting useCpuJit = coreSection.getSetting(SettingsFile.KEY_CPU_JIT); | ||||||
|  |         Setting hardwareRenderer = rendererSection.getSetting(SettingsFile.KEY_HW_RENDERER); | ||||||
|  |         Setting hardwareShader = rendererSection.getSetting(SettingsFile.KEY_HW_SHADER); | ||||||
|  |         Setting vsyncEnable = rendererSection.getSetting(SettingsFile.KEY_USE_VSYNC); | ||||||
|  | 
 | ||||||
|  |         sl.add(new HeaderSetting(null, null, R.string.debug_warning, 0)); | ||||||
|  |         sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, Settings.SECTION_CORE, R.string.cpu_jit, R.string.cpu_jit_description, true, useCpuJit, true, mView)); | ||||||
|  |         sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_RENDERER, Settings.SECTION_RENDERER, R.string.hw_renderer, R.string.hw_renderer_description, true, hardwareRenderer, true, mView)); | ||||||
|  |         sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, Settings.SECTION_RENDERER, R.string.hw_shaders, R.string.hw_shaders_description, true, hardwareShader, true, mView)); | ||||||
|  |         sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, Settings.SECTION_RENDERER, R.string.vsync, R.string.vsync_description, true, vsyncEnable)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,78 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui; | ||||||
|  | 
 | ||||||
|  | import androidx.fragment.app.FragmentActivity; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Settings; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Abstraction for a screen showing a list of settings. Instances of | ||||||
|  |  * this type of view will each display a layer of the setting hierarchy. | ||||||
|  |  */ | ||||||
|  | public interface SettingsFragmentView { | ||||||
|  |     /** | ||||||
|  |      * Called by the containing Activity to notify the Fragment that an | ||||||
|  |      * asynchronous load operation completed. | ||||||
|  |      * | ||||||
|  |      * @param settings The (possibly null) result of the ini load operation. | ||||||
|  |      */ | ||||||
|  |     void onSettingsFileLoaded(Settings settings); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Pass a settings HashMap to the containing activity, so that it can | ||||||
|  |      * share the HashMap with other SettingsFragments; useful so that rotations | ||||||
|  |      * do not require an additional load operation. | ||||||
|  |      * | ||||||
|  |      * @param settings An ArrayList containing all the settings HashMaps. | ||||||
|  |      */ | ||||||
|  |     void passSettingsToActivity(Settings settings); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Pass an ArrayList to the View so that it can be displayed on screen. | ||||||
|  |      * | ||||||
|  |      * @param settingsList The result of converting the HashMap to an ArrayList | ||||||
|  |      */ | ||||||
|  |     void showSettingsList(ArrayList<SettingsItem> settingsList); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called by the containing Activity when an asynchronous load operation fails. | ||||||
|  |      * Instructs the Fragment to load the settings screen with defaults selected. | ||||||
|  |      */ | ||||||
|  |     void loadDefaultSettings(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return The Fragment's containing activity. | ||||||
|  |      */ | ||||||
|  |     FragmentActivity getActivity(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Tell the Fragment to tell the containing Activity to show a new | ||||||
|  |      * Fragment containing a submenu of settings. | ||||||
|  |      * | ||||||
|  |      * @param menuKey Identifier for the settings group that should be shown. | ||||||
|  |      */ | ||||||
|  |     void loadSubMenu(String menuKey); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Tell the Fragment to tell the containing activity to display a toast message. | ||||||
|  |      * | ||||||
|  |      * @param message Text to be shown in the Toast | ||||||
|  |      * @param is_long Whether this should be a long Toast or short one. | ||||||
|  |      */ | ||||||
|  |     void showToastMessage(String message, boolean is_long); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Have the fragment add a setting to the HashMap. | ||||||
|  |      * | ||||||
|  |      * @param setting The (possibly previously missing) new setting. | ||||||
|  |      */ | ||||||
|  |     void putSetting(Setting setting); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Have the fragment tell the containing Activity that a setting was modified. | ||||||
|  |      */ | ||||||
|  |     void onSettingChanged(); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,48 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.util.AttributeSet; | ||||||
|  | import android.widget.FrameLayout; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * FrameLayout subclass with few Properties added to simplify animations. | ||||||
|  |  * Don't remove the methods appearing as unused, in order not to break the menu animations | ||||||
|  |  */ | ||||||
|  | public final class SettingsFrameLayout extends FrameLayout { | ||||||
|  |     private float mVisibleness = 1.0f; | ||||||
|  | 
 | ||||||
|  |     public SettingsFrameLayout(Context context) { | ||||||
|  |         super(context); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public SettingsFrameLayout(Context context, AttributeSet attrs) { | ||||||
|  |         super(context, attrs); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { | ||||||
|  |         super(context, attrs, defStyleAttr); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { | ||||||
|  |         super(context, attrs, defStyleAttr, defStyleRes); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public float getYFraction() { | ||||||
|  |         return getY() / getHeight(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setYFraction(float yFraction) { | ||||||
|  |         final int height = getHeight(); | ||||||
|  |         setY((height > 0) ? (yFraction * height) : -9999); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public float getVisibleness() { | ||||||
|  |         return mVisibleness; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setVisibleness(float visibleness) { | ||||||
|  |         setScaleX(visibleness); | ||||||
|  |         setScaleY(visibleness); | ||||||
|  |         setAlpha(visibleness); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,54 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui.viewholder; | ||||||
|  | 
 | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.CheckBox; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsAdapter; | ||||||
|  | 
 | ||||||
|  | public final class CheckBoxSettingViewHolder extends SettingViewHolder { | ||||||
|  |     private CheckBoxSetting mItem; | ||||||
|  | 
 | ||||||
|  |     private TextView mTextSettingName; | ||||||
|  |     private TextView mTextSettingDescription; | ||||||
|  | 
 | ||||||
|  |     private CheckBox mCheckbox; | ||||||
|  | 
 | ||||||
|  |     public CheckBoxSettingViewHolder(View itemView, SettingsAdapter adapter) { | ||||||
|  |         super(itemView, adapter); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void findViews(View root) { | ||||||
|  |         mTextSettingName = root.findViewById(R.id.text_setting_name); | ||||||
|  |         mTextSettingDescription = root.findViewById(R.id.text_setting_description); | ||||||
|  |         mCheckbox = root.findViewById(R.id.checkbox); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void bind(SettingsItem item) { | ||||||
|  |         mItem = (CheckBoxSetting) item; | ||||||
|  | 
 | ||||||
|  |         mTextSettingName.setText(item.getNameId()); | ||||||
|  | 
 | ||||||
|  |         if (item.getDescriptionId() > 0) { | ||||||
|  |             mTextSettingDescription.setText(item.getDescriptionId()); | ||||||
|  |             mTextSettingDescription.setVisibility(View.VISIBLE); | ||||||
|  |         } else { | ||||||
|  |             mTextSettingDescription.setText(""); | ||||||
|  |             mTextSettingDescription.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         mCheckbox.setChecked(mItem.isChecked()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onClick(View clicked) { | ||||||
|  |         mCheckbox.toggle(); | ||||||
|  | 
 | ||||||
|  |         getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked()); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,47 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui.viewholder; | ||||||
|  | 
 | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsAdapter; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | 
 | ||||||
|  | public final class DateTimeViewHolder extends SettingViewHolder { | ||||||
|  |     private DateTimeSetting mItem; | ||||||
|  | 
 | ||||||
|  |     private TextView mTextSettingName; | ||||||
|  |     private TextView mTextSettingDescription; | ||||||
|  | 
 | ||||||
|  |     public DateTimeViewHolder(View itemView, SettingsAdapter adapter) { | ||||||
|  |         super(itemView, adapter); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void findViews(View root) { | ||||||
|  |         mTextSettingName = root.findViewById(R.id.text_setting_name); | ||||||
|  |         Log.error("test " + mTextSettingName); | ||||||
|  |         mTextSettingDescription = root.findViewById(R.id.text_setting_description); | ||||||
|  |         Log.error("test " + mTextSettingDescription); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void bind(SettingsItem item) { | ||||||
|  |         mItem = (DateTimeSetting) item; | ||||||
|  |         mTextSettingName.setText(item.getNameId()); | ||||||
|  | 
 | ||||||
|  |         if (item.getDescriptionId() > 0) { | ||||||
|  |             mTextSettingDescription.setText(item.getDescriptionId()); | ||||||
|  |             mTextSettingDescription.setVisibility(View.VISIBLE); | ||||||
|  |         } else { | ||||||
|  |             mTextSettingDescription.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onClick(View clicked) { | ||||||
|  |         getAdapter().onDateTimeClick(mItem, getAdapterPosition()); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,32 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui.viewholder; | ||||||
|  | 
 | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsAdapter; | ||||||
|  | 
 | ||||||
|  | public final class HeaderViewHolder extends SettingViewHolder { | ||||||
|  |     private TextView mHeaderName; | ||||||
|  | 
 | ||||||
|  |     public HeaderViewHolder(View itemView, SettingsAdapter adapter) { | ||||||
|  |         super(itemView, adapter); | ||||||
|  |         itemView.setOnClickListener(null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void findViews(View root) { | ||||||
|  |         mHeaderName = root.findViewById(R.id.text_header_name); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void bind(SettingsItem item) { | ||||||
|  |         mHeaderName.setText(item.getNameId()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onClick(View clicked) { | ||||||
|  |         // no-op | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,55 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui.viewholder; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsAdapter; | ||||||
|  | 
 | ||||||
|  | public final class InputBindingSettingViewHolder extends SettingViewHolder { | ||||||
|  |     private InputBindingSetting mItem; | ||||||
|  | 
 | ||||||
|  |     private TextView mTextSettingName; | ||||||
|  |     private TextView mTextSettingDescription; | ||||||
|  | 
 | ||||||
|  |     private Context mContext; | ||||||
|  | 
 | ||||||
|  |     public InputBindingSettingViewHolder(View itemView, SettingsAdapter adapter, Context context) { | ||||||
|  |         super(itemView, adapter); | ||||||
|  | 
 | ||||||
|  |         mContext = context; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void findViews(View root) { | ||||||
|  |         mTextSettingName = root.findViewById(R.id.text_setting_name); | ||||||
|  |         mTextSettingDescription = root.findViewById(R.id.text_setting_description); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void bind(SettingsItem item) { | ||||||
|  |         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); | ||||||
|  | 
 | ||||||
|  |         mItem = (InputBindingSetting) item; | ||||||
|  | 
 | ||||||
|  |         mTextSettingName.setText(item.getNameId()); | ||||||
|  | 
 | ||||||
|  |         String key = sharedPreferences.getString(mItem.getKey(), ""); | ||||||
|  |         if (key != null && !key.isEmpty()) { | ||||||
|  |             mTextSettingDescription.setText(key); | ||||||
|  |             mTextSettingDescription.setVisibility(View.VISIBLE); | ||||||
|  |         } else { | ||||||
|  |             mTextSettingDescription.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onClick(View clicked) { | ||||||
|  |         getAdapter().onInputBindingClick(mItem, getAdapterPosition()); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,57 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui.viewholder; | ||||||
|  | 
 | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsAdapter; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsFragmentView; | ||||||
|  | import org.citra.citra_emu.ui.main.MainActivity; | ||||||
|  | 
 | ||||||
|  | public final class PremiumViewHolder extends SettingViewHolder { | ||||||
|  |     private TextView mHeaderName; | ||||||
|  |     private TextView mTextDescription; | ||||||
|  |     private SettingsFragmentView mView; | ||||||
|  | 
 | ||||||
|  |     public PremiumViewHolder(View itemView, SettingsAdapter adapter, SettingsFragmentView view) { | ||||||
|  |         super(itemView, adapter); | ||||||
|  |         mView = view; | ||||||
|  |         itemView.setOnClickListener(this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void findViews(View root) { | ||||||
|  |         mHeaderName = root.findViewById(R.id.text_setting_name); | ||||||
|  |         mTextDescription = root.findViewById(R.id.text_setting_description); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void bind(SettingsItem item) { | ||||||
|  |         updateText(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onClick(View clicked) { | ||||||
|  |         if (MainActivity.isPremiumActive()) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Invoke billing flow if Premium is not already active, then refresh the UI to indicate | ||||||
|  |         // the purchase has completed. | ||||||
|  |         MainActivity.invokePremiumBilling(() -> updateText()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Update the text shown to the user, based on whether Premium is active | ||||||
|  |      */ | ||||||
|  |     private void updateText() { | ||||||
|  |         if (MainActivity.isPremiumActive()) { | ||||||
|  |             mHeaderName.setText(R.string.premium_settings_welcome); | ||||||
|  |             mTextDescription.setText(R.string.premium_settings_welcome_description); | ||||||
|  |         } else { | ||||||
|  |             mHeaderName.setText(R.string.premium_settings_upsell); | ||||||
|  |             mTextDescription.setText(R.string.premium_settings_upsell_description); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,49 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui.viewholder; | ||||||
|  | 
 | ||||||
|  | import android.view.View; | ||||||
|  | 
 | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsAdapter; | ||||||
|  | 
 | ||||||
|  | public abstract class SettingViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { | ||||||
|  |     private SettingsAdapter mAdapter; | ||||||
|  | 
 | ||||||
|  |     public SettingViewHolder(View itemView, SettingsAdapter adapter) { | ||||||
|  |         super(itemView); | ||||||
|  | 
 | ||||||
|  |         mAdapter = adapter; | ||||||
|  | 
 | ||||||
|  |         itemView.setOnClickListener(this); | ||||||
|  | 
 | ||||||
|  |         findViews(itemView); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected SettingsAdapter getAdapter() { | ||||||
|  |         return mAdapter; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets handles to all this ViewHolder's child views using their XML-defined identifiers. | ||||||
|  |      * | ||||||
|  |      * @param root The newly inflated top-level view. | ||||||
|  |      */ | ||||||
|  |     protected abstract void findViews(View root); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called by the adapter to set this ViewHolder's child views to display the list item | ||||||
|  |      * it must now represent. | ||||||
|  |      * | ||||||
|  |      * @param item The list item that should be represented by this ViewHolder. | ||||||
|  |      */ | ||||||
|  |     public abstract void bind(SettingsItem item); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called when this ViewHolder's view is clicked on. Implementations should usually pass | ||||||
|  |      * this event up to the adapter. | ||||||
|  |      * | ||||||
|  |      * @param clicked The view that was clicked on. | ||||||
|  |      */ | ||||||
|  |     public abstract void onClick(View clicked); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,76 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui.viewholder; | ||||||
|  | 
 | ||||||
|  | import android.content.res.Resources; | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsAdapter; | ||||||
|  | 
 | ||||||
|  | public final class SingleChoiceViewHolder extends SettingViewHolder { | ||||||
|  |     private SettingsItem mItem; | ||||||
|  | 
 | ||||||
|  |     private TextView mTextSettingName; | ||||||
|  |     private TextView mTextSettingDescription; | ||||||
|  | 
 | ||||||
|  |     public SingleChoiceViewHolder(View itemView, SettingsAdapter adapter) { | ||||||
|  |         super(itemView, adapter); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void findViews(View root) { | ||||||
|  |         mTextSettingName = root.findViewById(R.id.text_setting_name); | ||||||
|  |         mTextSettingDescription = root.findViewById(R.id.text_setting_description); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void bind(SettingsItem item) { | ||||||
|  |         mItem = item; | ||||||
|  | 
 | ||||||
|  |         mTextSettingName.setText(item.getNameId()); | ||||||
|  |         mTextSettingDescription.setVisibility(View.VISIBLE); | ||||||
|  |         if (item.getDescriptionId() > 0) { | ||||||
|  |             mTextSettingDescription.setText(item.getDescriptionId()); | ||||||
|  |         } else if (item instanceof SingleChoiceSetting) { | ||||||
|  |             SingleChoiceSetting setting = (SingleChoiceSetting) item; | ||||||
|  |             int selected = setting.getSelectedValue(); | ||||||
|  |             Resources resMgr = mTextSettingDescription.getContext().getResources(); | ||||||
|  |             String[] choices = resMgr.getStringArray(setting.getChoicesId()); | ||||||
|  |             int[] values = resMgr.getIntArray(setting.getValuesId()); | ||||||
|  |             for (int i = 0; i < values.length; ++i) { | ||||||
|  |                 if (values[i] == selected) { | ||||||
|  |                     mTextSettingDescription.setText(choices[i]); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else if (item instanceof PremiumSingleChoiceSetting) { | ||||||
|  |             PremiumSingleChoiceSetting setting = (PremiumSingleChoiceSetting) item; | ||||||
|  |             int selected = setting.getSelectedValue(); | ||||||
|  |             Resources resMgr = mTextSettingDescription.getContext().getResources(); | ||||||
|  |             String[] choices = resMgr.getStringArray(setting.getChoicesId()); | ||||||
|  |             int[] values = resMgr.getIntArray(setting.getValuesId()); | ||||||
|  |             for (int i = 0; i < values.length; ++i) { | ||||||
|  |                 if (values[i] == selected) { | ||||||
|  |                     mTextSettingDescription.setText(choices[i]); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             mTextSettingDescription.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onClick(View clicked) { | ||||||
|  |         int position = getAdapterPosition(); | ||||||
|  |         if (mItem instanceof SingleChoiceSetting) { | ||||||
|  |             getAdapter().onSingleChoiceClick((SingleChoiceSetting) mItem, position); | ||||||
|  |         } else if (mItem instanceof PremiumSingleChoiceSetting) { | ||||||
|  |             getAdapter().onSingleChoiceClick((PremiumSingleChoiceSetting) mItem, position); | ||||||
|  |         } else if (mItem instanceof StringSingleChoiceSetting) { | ||||||
|  |             getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting) mItem, position); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,46 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui.viewholder; | ||||||
|  | 
 | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SliderSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsAdapter; | ||||||
|  | 
 | ||||||
|  | public final class SliderViewHolder extends SettingViewHolder { | ||||||
|  |     private SliderSetting mItem; | ||||||
|  | 
 | ||||||
|  |     private TextView mTextSettingName; | ||||||
|  |     private TextView mTextSettingDescription; | ||||||
|  | 
 | ||||||
|  |     public SliderViewHolder(View itemView, SettingsAdapter adapter) { | ||||||
|  |         super(itemView, adapter); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void findViews(View root) { | ||||||
|  |         mTextSettingName = root.findViewById(R.id.text_setting_name); | ||||||
|  |         mTextSettingDescription = root.findViewById(R.id.text_setting_description); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void bind(SettingsItem item) { | ||||||
|  |         mItem = (SliderSetting) item; | ||||||
|  | 
 | ||||||
|  |         mTextSettingName.setText(item.getNameId()); | ||||||
|  | 
 | ||||||
|  |         if (item.getDescriptionId() > 0) { | ||||||
|  |             mTextSettingDescription.setText(item.getDescriptionId()); | ||||||
|  |             mTextSettingDescription.setVisibility(View.VISIBLE); | ||||||
|  |         } else { | ||||||
|  |             mTextSettingDescription.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onClick(View clicked) { | ||||||
|  |         getAdapter().onSliderClick(mItem, getAdapterPosition()); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @ -0,0 +1,45 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.ui.viewholder; | ||||||
|  | 
 | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SettingsItem; | ||||||
|  | import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsAdapter; | ||||||
|  | 
 | ||||||
|  | public final class SubmenuViewHolder extends SettingViewHolder { | ||||||
|  |     private SubmenuSetting mItem; | ||||||
|  | 
 | ||||||
|  |     private TextView mTextSettingName; | ||||||
|  |     private TextView mTextSettingDescription; | ||||||
|  | 
 | ||||||
|  |     public SubmenuViewHolder(View itemView, SettingsAdapter adapter) { | ||||||
|  |         super(itemView, adapter); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void findViews(View root) { | ||||||
|  |         mTextSettingName = root.findViewById(R.id.text_setting_name); | ||||||
|  |         mTextSettingDescription = root.findViewById(R.id.text_setting_description); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void bind(SettingsItem item) { | ||||||
|  |         mItem = (SubmenuSetting) item; | ||||||
|  | 
 | ||||||
|  |         mTextSettingName.setText(item.getNameId()); | ||||||
|  | 
 | ||||||
|  |         if (item.getDescriptionId() > 0) { | ||||||
|  |             mTextSettingDescription.setText(item.getDescriptionId()); | ||||||
|  |             mTextSettingDescription.setVisibility(View.VISIBLE); | ||||||
|  |         } else { | ||||||
|  |             mTextSettingDescription.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onClick(View clicked) { | ||||||
|  |         getAdapter().onSubmenuClick(mItem); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,337 @@ | ||||||
|  | package org.citra.citra_emu.features.settings.utils; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.FloatSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.IntSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Setting; | ||||||
|  | import org.citra.citra_emu.features.settings.model.SettingSection; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Settings; | ||||||
|  | import org.citra.citra_emu.features.settings.model.StringSetting; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsActivityView; | ||||||
|  | import org.citra.citra_emu.utils.BiMap; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryInitialization; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | import org.ini4j.Wini; | ||||||
|  | 
 | ||||||
|  | import java.io.BufferedReader; | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.FileNotFoundException; | ||||||
|  | import java.io.FileReader; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.TreeMap; | ||||||
|  | import java.util.TreeSet; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Contains static methods for interacting with .ini files in which settings are stored. | ||||||
|  |  */ | ||||||
|  | public final class SettingsFile { | ||||||
|  |     public static final String FILE_NAME_CONFIG = "config"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_CPU_JIT = "use_cpu_jit"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_DESIGN = "design"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_PREMIUM = "premium"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_HW_RENDERER = "use_hw_renderer"; | ||||||
|  |     public static final String KEY_HW_SHADER = "use_hw_shader"; | ||||||
|  |     public static final String KEY_SHADERS_ACCURATE_MUL = "shaders_accurate_mul"; | ||||||
|  |     public static final String KEY_USE_SHADER_JIT = "use_shader_jit"; | ||||||
|  |     public static final String KEY_USE_DISK_SHADER_CACHE = "use_disk_shader_cache"; | ||||||
|  |     public static final String KEY_USE_VSYNC = "use_vsync_new"; | ||||||
|  |     public static final String KEY_RESOLUTION_FACTOR = "resolution_factor"; | ||||||
|  |     public static final String KEY_FRAME_LIMIT_ENABLED = "use_frame_limit"; | ||||||
|  |     public static final String KEY_FRAME_LIMIT = "frame_limit"; | ||||||
|  |     public static final String KEY_BACKGROUND_RED = "bg_red"; | ||||||
|  |     public static final String KEY_BACKGROUND_BLUE = "bg_blue"; | ||||||
|  |     public static final String KEY_BACKGROUND_GREEN = "bg_green"; | ||||||
|  |     public static final String KEY_RENDER_3D = "render_3d"; | ||||||
|  |     public static final String KEY_FACTOR_3D = "factor_3d"; | ||||||
|  |     public static final String KEY_PP_SHADER_NAME = "pp_shader_name"; | ||||||
|  |     public static final String KEY_FILTER_MODE = "filter_mode"; | ||||||
|  |     public static final String KEY_TEXTURE_FILTER_NAME = "texture_filter_name"; | ||||||
|  |     public static final String KEY_USE_ASYNCHRONOUS_GPU_EMULATION = "use_asynchronous_gpu_emulation"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_LAYOUT_OPTION = "layout_option"; | ||||||
|  |     public static final String KEY_SWAP_SCREEN = "swap_screen"; | ||||||
|  |     public static final String KEY_CARDBOARD_SCREEN_SIZE = "cardboard_screen_size"; | ||||||
|  |     public static final String KEY_CARDBOARD_X_SHIFT = "cardboard_x_shift"; | ||||||
|  |     public static final String KEY_CARDBOARD_Y_SHIFT = "cardboard_y_shift"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_AUDIO_OUTPUT_ENGINE = "output_engine"; | ||||||
|  |     public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching"; | ||||||
|  |     public static final String KEY_VOLUME = "volume"; | ||||||
|  |     public static final String KEY_MIC_INPUT_TYPE = "mic_input_type"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_IS_NEW_3DS = "is_new_3ds"; | ||||||
|  |     public static final String KEY_REGION_VALUE = "region_value"; | ||||||
|  |     public static final String KEY_LANGUAGE = "language"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_INIT_CLOCK = "init_clock"; | ||||||
|  |     public static final String KEY_INIT_TIME = "init_time"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_BUTTON_A = "button_a"; | ||||||
|  |     public static final String KEY_BUTTON_B = "button_b"; | ||||||
|  |     public static final String KEY_BUTTON_X = "button_x"; | ||||||
|  |     public static final String KEY_BUTTON_Y = "button_y"; | ||||||
|  |     public static final String KEY_BUTTON_SELECT = "button_select"; | ||||||
|  |     public static final String KEY_BUTTON_START = "button_start"; | ||||||
|  |     public static final String KEY_BUTTON_UP = "button_up"; | ||||||
|  |     public static final String KEY_BUTTON_DOWN = "button_down"; | ||||||
|  |     public static final String KEY_BUTTON_LEFT = "button_left"; | ||||||
|  |     public static final String KEY_BUTTON_RIGHT = "button_right"; | ||||||
|  |     public static final String KEY_BUTTON_L = "button_l"; | ||||||
|  |     public static final String KEY_BUTTON_R = "button_r"; | ||||||
|  |     public static final String KEY_BUTTON_ZL = "button_zl"; | ||||||
|  |     public static final String KEY_BUTTON_ZR = "button_zr"; | ||||||
|  |     public static final String KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical"; | ||||||
|  |     public static final String KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal"; | ||||||
|  |     public static final String KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical"; | ||||||
|  |     public static final String KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal"; | ||||||
|  |     public static final String KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"; | ||||||
|  |     public static final String KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"; | ||||||
|  |     public static final String KEY_CIRCLEPAD_UP = "circlepad_up"; | ||||||
|  |     public static final String KEY_CIRCLEPAD_DOWN = "circlepad_down"; | ||||||
|  |     public static final String KEY_CIRCLEPAD_LEFT = "circlepad_left"; | ||||||
|  |     public static final String KEY_CIRCLEPAD_RIGHT = "circlepad_right"; | ||||||
|  |     public static final String KEY_CSTICK_UP = "cstick_up"; | ||||||
|  |     public static final String KEY_CSTICK_DOWN = "cstick_down"; | ||||||
|  |     public static final String KEY_CSTICK_LEFT = "cstick_left"; | ||||||
|  |     public static final String KEY_CSTICK_RIGHT = "cstick_right"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_CAMERA_OUTER_RIGHT_NAME = "camera_outer_right_name"; | ||||||
|  |     public static final String KEY_CAMERA_OUTER_RIGHT_CONFIG = "camera_outer_right_config"; | ||||||
|  |     public static final String KEY_CAMERA_OUTER_RIGHT_FLIP = "camera_outer_right_flip"; | ||||||
|  |     public static final String KEY_CAMERA_OUTER_LEFT_NAME = "camera_outer_left_name"; | ||||||
|  |     public static final String KEY_CAMERA_OUTER_LEFT_CONFIG = "camera_outer_left_config"; | ||||||
|  |     public static final String KEY_CAMERA_OUTER_LEFT_FLIP = "camera_outer_left_flip"; | ||||||
|  |     public static final String KEY_CAMERA_INNER_NAME = "camera_inner_name"; | ||||||
|  |     public static final String KEY_CAMERA_INNER_CONFIG = "camera_inner_config"; | ||||||
|  |     public static final String KEY_CAMERA_INNER_FLIP = "camera_inner_flip"; | ||||||
|  | 
 | ||||||
|  |     public static final String KEY_LOG_FILTER = "log_filter"; | ||||||
|  | 
 | ||||||
|  |     private static BiMap<String, String> sectionsMap = new BiMap<>(); | ||||||
|  | 
 | ||||||
|  |     static { | ||||||
|  |         //TODO: Add members to sectionsMap when game-specific settings are added | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     private SettingsFile() { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves | ||||||
|  |      * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it | ||||||
|  |      * failed. | ||||||
|  |      * | ||||||
|  |      * @param ini          The ini file to load the settings from | ||||||
|  |      * @param isCustomGame | ||||||
|  |      * @param view         The current view. | ||||||
|  |      * @return An Observable that emits a HashMap of the file's contents, then completes. | ||||||
|  |      */ | ||||||
|  |     static HashMap<String, SettingSection> readFile(final File ini, boolean isCustomGame, SettingsActivityView view) { | ||||||
|  |         HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap(); | ||||||
|  | 
 | ||||||
|  |         BufferedReader reader = null; | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             reader = new BufferedReader(new FileReader(ini)); | ||||||
|  | 
 | ||||||
|  |             SettingSection current = null; | ||||||
|  |             for (String line; (line = reader.readLine()) != null; ) { | ||||||
|  |                 if (line.startsWith("[") && line.endsWith("]")) { | ||||||
|  |                     current = sectionFromLine(line, isCustomGame); | ||||||
|  |                     sections.put(current.getName(), current); | ||||||
|  |                 } else if ((current != null)) { | ||||||
|  |                     Setting setting = settingFromLine(current, line); | ||||||
|  |                     if (setting != null) { | ||||||
|  |                         current.putSetting(setting); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (FileNotFoundException e) { | ||||||
|  |             Log.error("[SettingsFile] File not found: " + ini.getAbsolutePath() + e.getMessage()); | ||||||
|  |             if (view != null) | ||||||
|  |                 view.onSettingsFileNotFound(); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             Log.error("[SettingsFile] Error reading from: " + ini.getAbsolutePath() + e.getMessage()); | ||||||
|  |             if (view != null) | ||||||
|  |                 view.onSettingsFileNotFound(); | ||||||
|  |         } finally { | ||||||
|  |             if (reader != null) { | ||||||
|  |                 try { | ||||||
|  |                     reader.close(); | ||||||
|  |                 } catch (IOException e) { | ||||||
|  |                     Log.error("[SettingsFile] Error closing: " + ini.getAbsolutePath() + e.getMessage()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return sections; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static HashMap<String, SettingSection> readFile(final String fileName, SettingsActivityView view) { | ||||||
|  |         return readFile(getSettingsFile(fileName), false, view); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves | ||||||
|  |      * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it | ||||||
|  |      * failed. | ||||||
|  |      * | ||||||
|  |      * @param gameId the id of the game to load it's settings. | ||||||
|  |      * @param view   The current view. | ||||||
|  |      */ | ||||||
|  |     public static HashMap<String, SettingSection> readCustomGameSettings(final String gameId, SettingsActivityView view) { | ||||||
|  |         return readFile(getCustomGameSettingsFile(gameId), true, view); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error | ||||||
|  |      * telling why it failed. | ||||||
|  |      * | ||||||
|  |      * @param fileName The target filename without a path or extension. | ||||||
|  |      * @param sections The HashMap containing the Settings we want to serialize. | ||||||
|  |      * @param view     The current view. | ||||||
|  |      */ | ||||||
|  |     public static void saveFile(final String fileName, TreeMap<String, SettingSection> sections, | ||||||
|  |                                 SettingsActivityView view) { | ||||||
|  |         File ini = getSettingsFile(fileName); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             Wini writer = new Wini(ini); | ||||||
|  | 
 | ||||||
|  |             Set<String> keySet = sections.keySet(); | ||||||
|  |             for (String key : keySet) { | ||||||
|  |                 SettingSection section = sections.get(key); | ||||||
|  |                 writeSection(writer, section); | ||||||
|  |             } | ||||||
|  |             writer.store(); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage()); | ||||||
|  |             view.showToastMessage(CitraApplication.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     public static void saveCustomGameSettings(final String gameId, final HashMap<String, SettingSection> sections) { | ||||||
|  |         Set<String> sortedSections = new TreeSet<>(sections.keySet()); | ||||||
|  | 
 | ||||||
|  |         for (String sectionKey : sortedSections) { | ||||||
|  |             SettingSection section = sections.get(sectionKey); | ||||||
|  | 
 | ||||||
|  |             HashMap<String, Setting> settings = section.getSettings(); | ||||||
|  |             Set<String> sortedKeySet = new TreeSet<>(settings.keySet()); | ||||||
|  | 
 | ||||||
|  |             for (String settingKey : sortedKeySet) { | ||||||
|  |                 Setting setting = settings.get(settingKey); | ||||||
|  |                 NativeLibrary.SetUserSetting(gameId, mapSectionNameFromIni(section.getName()), setting.getKey(), setting.getValueAsString()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static String mapSectionNameFromIni(String generalSectionName) { | ||||||
|  |         if (sectionsMap.getForward(generalSectionName) != null) { | ||||||
|  |             return sectionsMap.getForward(generalSectionName); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return generalSectionName; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static String mapSectionNameToIni(String generalSectionName) { | ||||||
|  |         if (sectionsMap.getBackward(generalSectionName) != null) { | ||||||
|  |             return sectionsMap.getBackward(generalSectionName); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return generalSectionName; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private static File getSettingsFile(String fileName) { | ||||||
|  |         return new File( | ||||||
|  |                 DirectoryInitialization.getUserDirectory() + "/config/" + fileName + ".ini"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static File getCustomGameSettingsFile(String gameId) { | ||||||
|  |         return new File(DirectoryInitialization.getUserDirectory() + "/GameSettings/" + gameId + ".ini"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static SettingSection sectionFromLine(String line, boolean isCustomGame) { | ||||||
|  |         String sectionName = line.substring(1, line.length() - 1); | ||||||
|  |         if (isCustomGame) { | ||||||
|  |             sectionName = mapSectionNameToIni(sectionName); | ||||||
|  |         } | ||||||
|  |         return new SettingSection(sectionName); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * For a line of text, determines what type of data is being represented, and returns | ||||||
|  |      * a Setting object containing this data. | ||||||
|  |      * | ||||||
|  |      * @param current The section currently being parsed by the consuming method. | ||||||
|  |      * @param line    The line of text being parsed. | ||||||
|  |      * @return A typed Setting containing the key/value contained in the line. | ||||||
|  |      */ | ||||||
|  |     private static Setting settingFromLine(SettingSection current, String line) { | ||||||
|  |         String[] splitLine = line.split("="); | ||||||
|  | 
 | ||||||
|  |         if (splitLine.length != 2) { | ||||||
|  |             Log.warning("Skipping invalid config line \"" + line + "\""); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         String key = splitLine[0].trim(); | ||||||
|  |         String value = splitLine[1].trim(); | ||||||
|  | 
 | ||||||
|  |         if (value.isEmpty()) { | ||||||
|  |             Log.warning("Skipping null value in config line \"" + line + "\""); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             int valueAsInt = Integer.parseInt(value); | ||||||
|  | 
 | ||||||
|  |             return new IntSetting(key, current.getName(), valueAsInt); | ||||||
|  |         } catch (NumberFormatException ex) { | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             float valueAsFloat = Float.parseFloat(value); | ||||||
|  | 
 | ||||||
|  |             return new FloatSetting(key, current.getName(), valueAsFloat); | ||||||
|  |         } catch (NumberFormatException ex) { | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return new StringSetting(key, current.getName(), value); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Writes the contents of a Section HashMap to disk. | ||||||
|  |      * | ||||||
|  |      * @param parser  A Wini pointed at a file on disk. | ||||||
|  |      * @param section A section containing settings to be written to the file. | ||||||
|  |      */ | ||||||
|  |     private static void writeSection(Wini parser, SettingSection section) { | ||||||
|  |         // Write the section header. | ||||||
|  |         String header = section.getName(); | ||||||
|  | 
 | ||||||
|  |         // Write this section's values. | ||||||
|  |         HashMap<String, Setting> settings = section.getSettings(); | ||||||
|  |         Set<String> keySet = settings.keySet(); | ||||||
|  | 
 | ||||||
|  |         for (String key : keySet) { | ||||||
|  |             Setting setting = settings.get(key); | ||||||
|  |             parser.put(header, setting.getKey(), setting.getValueAsString()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,120 @@ | ||||||
|  | package org.citra.citra_emu.fragments; | ||||||
|  | 
 | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.os.Environment; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.appcompat.widget.Toolbar; | ||||||
|  | import androidx.core.content.FileProvider; | ||||||
|  | 
 | ||||||
|  | import com.nononsenseapps.filepicker.FilePickerFragment; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | public class CustomFilePickerFragment extends FilePickerFragment { | ||||||
|  |     private static String ALL_FILES = "*"; | ||||||
|  |     private int mTitle; | ||||||
|  |     private static List<String> extensions = Collections.singletonList(ALL_FILES); | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Uri toUri(@NonNull final File file) { | ||||||
|  |         return FileProvider | ||||||
|  |                 .getUriForFile(getContext(), | ||||||
|  |                         getContext().getApplicationContext().getPackageName() + ".filesprovider", | ||||||
|  |                         file); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onActivityCreated(Bundle savedInstanceState) { | ||||||
|  |         super.onActivityCreated(savedInstanceState); | ||||||
|  | 
 | ||||||
|  |         if (mode == MODE_DIR) { | ||||||
|  |             TextView ok = getActivity().findViewById(R.id.nnf_button_ok); | ||||||
|  |             ok.setText(R.string.select_dir); | ||||||
|  | 
 | ||||||
|  |             TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel); | ||||||
|  |             cancel.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected View inflateRootView(LayoutInflater inflater, ViewGroup container) { | ||||||
|  |         View view = super.inflateRootView(inflater, container); | ||||||
|  |         if (mTitle != 0) { | ||||||
|  |             Toolbar toolbar = view.findViewById(com.nononsenseapps.filepicker.R.id.nnf_picker_toolbar); | ||||||
|  |             ViewGroup parent = (ViewGroup) toolbar.getParent(); | ||||||
|  |             int index = parent.indexOfChild(toolbar); | ||||||
|  |             View newToolbar = inflater.inflate(R.layout.filepicker_toolbar, toolbar, false); | ||||||
|  |             TextView title = newToolbar.findViewById(R.id.filepicker_title); | ||||||
|  |             title.setText(mTitle); | ||||||
|  |             parent.removeView(toolbar); | ||||||
|  |             parent.addView(newToolbar, index); | ||||||
|  |         } | ||||||
|  |         return view; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setTitle(int title) { | ||||||
|  |         mTitle = title; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setAllowedExtensions(String allowedExtensions) { | ||||||
|  |         if (allowedExtensions == null) | ||||||
|  |             return; | ||||||
|  | 
 | ||||||
|  |         extensions = Arrays.asList(allowedExtensions.split(",")); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected boolean isItemVisible(@NonNull final File file) { | ||||||
|  |         // Some users jump to the conclusion that Dolphin isn't able to detect their | ||||||
|  |         // files if the files don't show up in the file picker when mode == MODE_DIR. | ||||||
|  |         // To avoid this, show files even when the user needs to select a directory. | ||||||
|  |         return (showHiddenItems || !file.isHidden()) && | ||||||
|  |                 (file.isDirectory() || extensions.contains(ALL_FILES) || | ||||||
|  |                         extensions.contains(fileExtension(file.getName()).toLowerCase())); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean isCheckable(@NonNull final File file) { | ||||||
|  |         // We need to make a small correction to the isCheckable logic due to | ||||||
|  |         // overriding isItemVisible to show files when mode == MODE_DIR. | ||||||
|  |         // AbstractFilePickerFragment always treats files as checkable when | ||||||
|  |         // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR. | ||||||
|  |         return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void goUp() { | ||||||
|  |         if (Environment.getExternalStorageDirectory().getPath().equals(mCurrentPath.getPath())) { | ||||||
|  |             goToDir(new File("/storage/")); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         if (mCurrentPath.equals(new File("/storage/"))){ | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         super.goUp(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) { | ||||||
|  |         if(viewHolder.file.equals(new File("/storage/emulated/"))) | ||||||
|  |             viewHolder.file = new File("/storage/emulated/0/"); | ||||||
|  |         super.onClickDir(view, viewHolder); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static String fileExtension(@NonNull String filename) { | ||||||
|  |         int i = filename.lastIndexOf('.'); | ||||||
|  |         return i < 0 ? "" : filename.substring(i + 1); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,378 @@ | ||||||
|  | package org.citra.citra_emu.fragments; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.IntentFilter; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.os.Handler; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | import android.view.Choreographer; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.Surface; | ||||||
|  | import android.view.SurfaceHolder; | ||||||
|  | import android.view.SurfaceView; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.widget.Button; | ||||||
|  | import android.widget.TextView; | ||||||
|  | import android.widget.Toast; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.fragment.app.Fragment; | ||||||
|  | import androidx.localbroadcastmanager.content.LocalBroadcastManager; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.activities.EmulationActivity; | ||||||
|  | import org.citra.citra_emu.overlay.InputOverlay; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryInitialization; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryStateReceiver; | ||||||
|  | import org.citra.citra_emu.utils.EmulationMenuSettings; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | 
 | ||||||
|  | public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback, Choreographer.FrameCallback { | ||||||
|  |     private static final String KEY_GAMEPATH = "gamepath"; | ||||||
|  | 
 | ||||||
|  |     private static final Handler perfStatsUpdateHandler = new Handler(); | ||||||
|  | 
 | ||||||
|  |     private SharedPreferences mPreferences; | ||||||
|  | 
 | ||||||
|  |     private InputOverlay mInputOverlay; | ||||||
|  | 
 | ||||||
|  |     private EmulationState mEmulationState; | ||||||
|  | 
 | ||||||
|  |     private DirectoryStateReceiver directoryStateReceiver; | ||||||
|  | 
 | ||||||
|  |     private EmulationActivity activity; | ||||||
|  | 
 | ||||||
|  |     private TextView mPerfStats; | ||||||
|  | 
 | ||||||
|  |     private Runnable perfStatsUpdater; | ||||||
|  | 
 | ||||||
|  |     public static EmulationFragment newInstance(String gamePath) { | ||||||
|  |         Bundle args = new Bundle(); | ||||||
|  |         args.putString(KEY_GAMEPATH, gamePath); | ||||||
|  | 
 | ||||||
|  |         EmulationFragment fragment = new EmulationFragment(); | ||||||
|  |         fragment.setArguments(args); | ||||||
|  |         return fragment; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onAttach(@NonNull Context context) { | ||||||
|  |         super.onAttach(context); | ||||||
|  | 
 | ||||||
|  |         if (context instanceof EmulationActivity) { | ||||||
|  |             activity = (EmulationActivity) context; | ||||||
|  |             NativeLibrary.setEmulationActivity((EmulationActivity) context); | ||||||
|  |         } else { | ||||||
|  |             throw new IllegalStateException("EmulationFragment must have EmulationActivity parent"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize anything that doesn't depend on the layout / views in here. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  | 
 | ||||||
|  |         // So this fragment doesn't restart on configuration changes; i.e. rotation. | ||||||
|  |         setRetainInstance(true); | ||||||
|  | 
 | ||||||
|  |         mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); | ||||||
|  | 
 | ||||||
|  |         String gamePath = getArguments().getString(KEY_GAMEPATH); | ||||||
|  |         mEmulationState = new EmulationState(gamePath); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize the UI and start emulation in here. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||||
|  |         View contents = inflater.inflate(R.layout.fragment_emulation, container, false); | ||||||
|  | 
 | ||||||
|  |         SurfaceView surfaceView = contents.findViewById(R.id.surface_emulation); | ||||||
|  |         surfaceView.getHolder().addCallback(this); | ||||||
|  | 
 | ||||||
|  |         mInputOverlay = contents.findViewById(R.id.surface_input_overlay); | ||||||
|  |         mPerfStats = contents.findViewById(R.id.show_fps_text); | ||||||
|  | 
 | ||||||
|  |         Button doneButton = contents.findViewById(R.id.done_control_config); | ||||||
|  |         if (doneButton != null) { | ||||||
|  |             doneButton.setOnClickListener(v -> stopConfiguringControls()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Show/hide the "Show FPS" overlay | ||||||
|  |         updateShowFpsOverlay(); | ||||||
|  | 
 | ||||||
|  |         // The new Surface created here will get passed to the native code via onSurfaceChanged. | ||||||
|  |         return contents; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onResume() { | ||||||
|  |         super.onResume(); | ||||||
|  |         Choreographer.getInstance().postFrameCallback(this); | ||||||
|  |         if (DirectoryInitialization.areCitraDirectoriesReady()) { | ||||||
|  |             mEmulationState.run(activity.isActivityRecreated()); | ||||||
|  |         } else { | ||||||
|  |             setupCitraDirectoriesThenStartEmulation(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onPause() { | ||||||
|  |         if (directoryStateReceiver != null) { | ||||||
|  |             LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver); | ||||||
|  |             directoryStateReceiver = null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (mEmulationState.isRunning()) { | ||||||
|  |             mEmulationState.pause(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Choreographer.getInstance().removeFrameCallback(this); | ||||||
|  |         super.onPause(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onDetach() { | ||||||
|  |         NativeLibrary.clearEmulationActivity(); | ||||||
|  |         super.onDetach(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setupCitraDirectoriesThenStartEmulation() { | ||||||
|  |         IntentFilter statusIntentFilter = new IntentFilter( | ||||||
|  |                 DirectoryInitialization.BROADCAST_ACTION); | ||||||
|  | 
 | ||||||
|  |         directoryStateReceiver = | ||||||
|  |                 new DirectoryStateReceiver(directoryInitializationState -> | ||||||
|  |                 { | ||||||
|  |                     if (directoryInitializationState == | ||||||
|  |                             DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) { | ||||||
|  |                         mEmulationState.run(activity.isActivityRecreated()); | ||||||
|  |                     } else if (directoryInitializationState == | ||||||
|  |                             DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) { | ||||||
|  |                         Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT) | ||||||
|  |                                 .show(); | ||||||
|  |                     } else if (directoryInitializationState == | ||||||
|  |                             DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { | ||||||
|  |                         Toast.makeText(getContext(), R.string.external_storage_not_mounted, | ||||||
|  |                                 Toast.LENGTH_SHORT) | ||||||
|  |                                 .show(); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |         // Registers the DirectoryStateReceiver and its intent filters | ||||||
|  |         LocalBroadcastManager.getInstance(getActivity()).registerReceiver( | ||||||
|  |                 directoryStateReceiver, | ||||||
|  |                 statusIntentFilter); | ||||||
|  |         DirectoryInitialization.start(getActivity()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void refreshInputOverlay() { | ||||||
|  |         mInputOverlay.refreshControls(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void resetInputOverlay() { | ||||||
|  |         // Reset button scale | ||||||
|  |         SharedPreferences.Editor editor = mPreferences.edit(); | ||||||
|  |         editor.putInt("controlScale", 50); | ||||||
|  |         editor.apply(); | ||||||
|  | 
 | ||||||
|  |         mInputOverlay.resetButtonPlacement(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void updateShowFpsOverlay() { | ||||||
|  |         if (EmulationMenuSettings.getShowFps()) { | ||||||
|  |             final int SYSTEM_FPS = 0; | ||||||
|  |             final int FPS = 1; | ||||||
|  |             final int FRAMETIME = 2; | ||||||
|  |             final int SPEED = 3; | ||||||
|  | 
 | ||||||
|  |             perfStatsUpdater = () -> | ||||||
|  |             { | ||||||
|  |                 final double[] perfStats = NativeLibrary.GetPerfStats(); | ||||||
|  |                 if (perfStats[FPS] > 0) { | ||||||
|  |                     mPerfStats.setText(String.format("FPS: %d Speed: %d%%", (int) (perfStats[FPS] + 0.5), | ||||||
|  |                             (int) (perfStats[SPEED] * 100.0 + 0.5))); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 3000); | ||||||
|  |             }; | ||||||
|  |             perfStatsUpdateHandler.post(perfStatsUpdater); | ||||||
|  | 
 | ||||||
|  |             mPerfStats.setVisibility(View.VISIBLE); | ||||||
|  |         } else { | ||||||
|  |             if (perfStatsUpdater != null) { | ||||||
|  |                 perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             mPerfStats.setVisibility(View.GONE); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void surfaceCreated(SurfaceHolder holder) { | ||||||
|  |         // We purposely don't do anything here. | ||||||
|  |         // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { | ||||||
|  |         Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height); | ||||||
|  |         mEmulationState.newSurface(holder.getSurface()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void surfaceDestroyed(SurfaceHolder holder) { | ||||||
|  |         mEmulationState.clearSurface(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void doFrame(long frameTimeNanos) { | ||||||
|  |         Choreographer.getInstance().postFrameCallback(this); | ||||||
|  |         NativeLibrary.DoFrame(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void stopEmulation() { | ||||||
|  |         mEmulationState.stop(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void startConfiguringControls() { | ||||||
|  |         getView().findViewById(R.id.done_control_config).setVisibility(View.VISIBLE); | ||||||
|  |         mInputOverlay.setIsInEditMode(true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void stopConfiguringControls() { | ||||||
|  |         getView().findViewById(R.id.done_control_config).setVisibility(View.GONE); | ||||||
|  |         mInputOverlay.setIsInEditMode(false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isConfiguringControls() { | ||||||
|  |         return mInputOverlay.isInEditMode(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static class EmulationState { | ||||||
|  |         private final String mGamePath; | ||||||
|  |         private State state; | ||||||
|  |         private Surface mSurface; | ||||||
|  |         private boolean mRunWhenSurfaceIsValid; | ||||||
|  | 
 | ||||||
|  |         EmulationState(String gamePath) { | ||||||
|  |             mGamePath = gamePath; | ||||||
|  |             // Starting state is stopped. | ||||||
|  |             state = State.STOPPED; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public synchronized boolean isStopped() { | ||||||
|  |             return state == State.STOPPED; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Getters for the current state | ||||||
|  | 
 | ||||||
|  |         public synchronized boolean isPaused() { | ||||||
|  |             return state == State.PAUSED; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public synchronized boolean isRunning() { | ||||||
|  |             return state == State.RUNNING; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public synchronized void stop() { | ||||||
|  |             if (state != State.STOPPED) { | ||||||
|  |                 Log.debug("[EmulationFragment] Stopping emulation."); | ||||||
|  |                 state = State.STOPPED; | ||||||
|  |                 NativeLibrary.StopEmulation(); | ||||||
|  |             } else { | ||||||
|  |                 Log.warning("[EmulationFragment] Stop called while already stopped."); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // State changing methods | ||||||
|  | 
 | ||||||
|  |         public synchronized void pause() { | ||||||
|  |             if (state != State.PAUSED) { | ||||||
|  |                 state = State.PAUSED; | ||||||
|  |                 Log.debug("[EmulationFragment] Pausing emulation."); | ||||||
|  | 
 | ||||||
|  |                 // Release the surface before pausing, since emulation has to be running for that. | ||||||
|  |                 NativeLibrary.SurfaceDestroyed(); | ||||||
|  |                 NativeLibrary.PauseEmulation(); | ||||||
|  |             } else { | ||||||
|  |                 Log.warning("[EmulationFragment] Pause called while already paused."); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public synchronized void run(boolean isActivityRecreated) { | ||||||
|  |             if (isActivityRecreated) { | ||||||
|  |                 if (NativeLibrary.IsRunning()) { | ||||||
|  |                     state = State.PAUSED; | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 Log.debug("[EmulationFragment] activity resumed or fresh start"); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // If the surface is set, run now. Otherwise, wait for it to get set. | ||||||
|  |             if (mSurface != null) { | ||||||
|  |                 runWithValidSurface(); | ||||||
|  |             } else { | ||||||
|  |                 mRunWhenSurfaceIsValid = true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Surface callbacks | ||||||
|  |         public synchronized void newSurface(Surface surface) { | ||||||
|  |             mSurface = surface; | ||||||
|  |             if (mRunWhenSurfaceIsValid) { | ||||||
|  |                 runWithValidSurface(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public synchronized void clearSurface() { | ||||||
|  |             if (mSurface == null) { | ||||||
|  |                 Log.warning("[EmulationFragment] clearSurface called, but surface already null."); | ||||||
|  |             } else { | ||||||
|  |                 mSurface = null; | ||||||
|  |                 Log.debug("[EmulationFragment] Surface destroyed."); | ||||||
|  | 
 | ||||||
|  |                 if (state == State.RUNNING) { | ||||||
|  |                     NativeLibrary.SurfaceDestroyed(); | ||||||
|  |                     state = State.PAUSED; | ||||||
|  |                 } else if (state == State.PAUSED) { | ||||||
|  |                     Log.warning("[EmulationFragment] Surface cleared while emulation paused."); | ||||||
|  |                 } else { | ||||||
|  |                     Log.warning("[EmulationFragment] Surface cleared while emulation stopped."); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private void runWithValidSurface() { | ||||||
|  |             mRunWhenSurfaceIsValid = false; | ||||||
|  |             if (state == State.STOPPED) { | ||||||
|  |                 NativeLibrary.SurfaceChanged(mSurface); | ||||||
|  |                 Thread mEmulationThread = new Thread(() -> | ||||||
|  |                 { | ||||||
|  |                     Log.debug("[EmulationFragment] Starting emulation thread."); | ||||||
|  |                     NativeLibrary.Run(mGamePath); | ||||||
|  |                 }, "NativeEmulation"); | ||||||
|  |                 mEmulationThread.start(); | ||||||
|  | 
 | ||||||
|  |             } else if (state == State.PAUSED) { | ||||||
|  |                 Log.debug("[EmulationFragment] Resuming emulation."); | ||||||
|  |                 NativeLibrary.SurfaceChanged(mSurface); | ||||||
|  |                 NativeLibrary.UnPauseEmulation(); | ||||||
|  |             } else { | ||||||
|  |                 Log.debug("[EmulationFragment] Bug, run called while already running."); | ||||||
|  |             } | ||||||
|  |             state = State.RUNNING; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private enum State { | ||||||
|  |             STOPPED, RUNNING, PAUSED | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,76 @@ | ||||||
|  | package org.citra.citra_emu.model; | ||||||
|  | 
 | ||||||
|  | import android.content.ContentValues; | ||||||
|  | import android.database.Cursor; | ||||||
|  | 
 | ||||||
|  | import java.nio.file.Paths; | ||||||
|  | 
 | ||||||
|  | public final class Game { | ||||||
|  |     private String mTitle; | ||||||
|  |     private String mDescription; | ||||||
|  |     private String mPath; | ||||||
|  |     private String mGameId; | ||||||
|  |     private String mCompany; | ||||||
|  |     private String mRegions; | ||||||
|  | 
 | ||||||
|  |     public Game(String title, String description, String regions, String path, | ||||||
|  |                 String gameId, String company) { | ||||||
|  |         mTitle = title; | ||||||
|  |         mDescription = description; | ||||||
|  |         mRegions = regions; | ||||||
|  |         mPath = path; | ||||||
|  |         mGameId = gameId; | ||||||
|  |         mCompany = company; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static ContentValues asContentValues(String title, String description, String regions, String path, String gameId, String company) { | ||||||
|  |         ContentValues values = new ContentValues(); | ||||||
|  | 
 | ||||||
|  |         if (gameId.isEmpty()) { | ||||||
|  |             // Homebrew, etc. may not have a game ID, use filename as a unique identifier | ||||||
|  |             gameId = Paths.get(path).getFileName().toString(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         values.put(GameDatabase.KEY_GAME_TITLE, title); | ||||||
|  |         values.put(GameDatabase.KEY_GAME_DESCRIPTION, description); | ||||||
|  |         values.put(GameDatabase.KEY_GAME_REGIONS, regions); | ||||||
|  |         values.put(GameDatabase.KEY_GAME_PATH, path); | ||||||
|  |         values.put(GameDatabase.KEY_GAME_ID, gameId); | ||||||
|  |         values.put(GameDatabase.KEY_GAME_COMPANY, company); | ||||||
|  | 
 | ||||||
|  |         return values; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static Game fromCursor(Cursor cursor) { | ||||||
|  |         return new Game(cursor.getString(GameDatabase.GAME_COLUMN_TITLE), | ||||||
|  |                 cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION), | ||||||
|  |                 cursor.getString(GameDatabase.GAME_COLUMN_REGIONS), | ||||||
|  |                 cursor.getString(GameDatabase.GAME_COLUMN_PATH), | ||||||
|  |                 cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID), | ||||||
|  |                 cursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getTitle() { | ||||||
|  |         return mTitle; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getDescription() { | ||||||
|  |         return mDescription; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getCompany() { | ||||||
|  |         return mCompany; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getRegions() { | ||||||
|  |         return mRegions; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getPath() { | ||||||
|  |         return mPath; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getGameId() { | ||||||
|  |         return mGameId; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,280 @@ | ||||||
|  | package org.citra.citra_emu.model; | ||||||
|  | 
 | ||||||
|  | import android.content.ContentValues; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.database.Cursor; | ||||||
|  | import android.database.sqlite.SQLiteDatabase; | ||||||
|  | import android.database.sqlite.SQLiteOpenHelper; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.lang.reflect.Array; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.HashSet; | ||||||
|  | import java.util.Set; | ||||||
|  | 
 | ||||||
|  | import rx.Observable; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A helper class that provides several utilities simplifying interaction with | ||||||
|  |  * the SQLite database. | ||||||
|  |  */ | ||||||
|  | public final class GameDatabase extends SQLiteOpenHelper { | ||||||
|  |     public static final int COLUMN_DB_ID = 0; | ||||||
|  |     public static final int GAME_COLUMN_PATH = 1; | ||||||
|  |     public static final int GAME_COLUMN_TITLE = 2; | ||||||
|  |     public static final int GAME_COLUMN_DESCRIPTION = 3; | ||||||
|  |     public static final int GAME_COLUMN_REGIONS = 4; | ||||||
|  |     public static final int GAME_COLUMN_GAME_ID = 5; | ||||||
|  |     public static final int GAME_COLUMN_COMPANY = 6; | ||||||
|  |     public static final int FOLDER_COLUMN_PATH = 1; | ||||||
|  |     public static final String KEY_DB_ID = "_id"; | ||||||
|  |     public static final String KEY_GAME_PATH = "path"; | ||||||
|  |     public static final String KEY_GAME_TITLE = "title"; | ||||||
|  |     public static final String KEY_GAME_DESCRIPTION = "description"; | ||||||
|  |     public static final String KEY_GAME_REGIONS = "regions"; | ||||||
|  |     public static final String KEY_GAME_ID = "game_id"; | ||||||
|  |     public static final String KEY_GAME_COMPANY = "company"; | ||||||
|  |     public static final String KEY_FOLDER_PATH = "path"; | ||||||
|  |     public static final String TABLE_NAME_FOLDERS = "folders"; | ||||||
|  |     public static final String TABLE_NAME_GAMES = "games"; | ||||||
|  |     private static final int DB_VERSION = 2; | ||||||
|  |     private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY"; | ||||||
|  |     private static final String TYPE_INTEGER = " INTEGER"; | ||||||
|  |     private static final String TYPE_STRING = " TEXT"; | ||||||
|  | 
 | ||||||
|  |     private static final String CONSTRAINT_UNIQUE = " UNIQUE"; | ||||||
|  | 
 | ||||||
|  |     private static final String SEPARATOR = ", "; | ||||||
|  | 
 | ||||||
|  |     private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "(" | ||||||
|  |             + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR | ||||||
|  |             + KEY_GAME_PATH + TYPE_STRING + SEPARATOR | ||||||
|  |             + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR | ||||||
|  |             + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR | ||||||
|  |             + KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR | ||||||
|  |             + KEY_GAME_ID + TYPE_STRING + SEPARATOR | ||||||
|  |             + KEY_GAME_COMPANY + TYPE_STRING + ")"; | ||||||
|  | 
 | ||||||
|  |     private static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "(" | ||||||
|  |             + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR | ||||||
|  |             + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")"; | ||||||
|  | 
 | ||||||
|  |     private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS; | ||||||
|  |     private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES; | ||||||
|  | 
 | ||||||
|  |     public GameDatabase(Context context) { | ||||||
|  |         // Superclass constructor builds a database or uses an existing one. | ||||||
|  |         super(context, "games.db", null, DB_VERSION); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(SQLiteDatabase database) { | ||||||
|  |         Log.debug("[GameDatabase] GameDatabase - Creating database..."); | ||||||
|  | 
 | ||||||
|  |         execSqlAndLog(database, SQL_CREATE_GAMES); | ||||||
|  |         execSqlAndLog(database, SQL_CREATE_FOLDERS); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion) { | ||||||
|  |         Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases.."); | ||||||
|  |         execSqlAndLog(database, SQL_DELETE_FOLDERS); | ||||||
|  |         execSqlAndLog(database, SQL_CREATE_FOLDERS); | ||||||
|  | 
 | ||||||
|  |         execSqlAndLog(database, SQL_DELETE_GAMES); | ||||||
|  |         execSqlAndLog(database, SQL_CREATE_GAMES); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { | ||||||
|  |         Log.info("[GameDatabase] Upgrading database from schema version " + oldVersion + " to " + | ||||||
|  |                 newVersion); | ||||||
|  | 
 | ||||||
|  |         // Delete all the games | ||||||
|  |         execSqlAndLog(database, SQL_DELETE_GAMES); | ||||||
|  |         execSqlAndLog(database, SQL_CREATE_GAMES); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void resetDatabase(SQLiteDatabase database) { | ||||||
|  |         execSqlAndLog(database, SQL_DELETE_FOLDERS); | ||||||
|  |         execSqlAndLog(database, SQL_CREATE_FOLDERS); | ||||||
|  | 
 | ||||||
|  |         execSqlAndLog(database, SQL_DELETE_GAMES); | ||||||
|  |         execSqlAndLog(database, SQL_CREATE_GAMES); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void scanLibrary(SQLiteDatabase database) { | ||||||
|  |         // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing. | ||||||
|  |         Cursor fileCursor = database.query(TABLE_NAME_GAMES, | ||||||
|  |                 null,    // Get all columns. | ||||||
|  |                 null,    // Get all rows. | ||||||
|  |                 null, | ||||||
|  |                 null,    // No grouping. | ||||||
|  |                 null, | ||||||
|  |                 null);    // Order of games is irrelevant. | ||||||
|  | 
 | ||||||
|  |         // Possibly overly defensive, but ensures that moveToNext() does not skip a row. | ||||||
|  |         fileCursor.moveToPosition(-1); | ||||||
|  | 
 | ||||||
|  |         while (fileCursor.moveToNext()) { | ||||||
|  |             String gamePath = fileCursor.getString(GAME_COLUMN_PATH); | ||||||
|  |             File game = new File(gamePath); | ||||||
|  | 
 | ||||||
|  |             if (!game.exists()) { | ||||||
|  |                 Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " + | ||||||
|  |                         gamePath); | ||||||
|  |                 database.delete(TABLE_NAME_GAMES, | ||||||
|  |                         KEY_DB_ID + " = ?", | ||||||
|  |                         new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))}); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Get a cursor listing all the folders the user has added to the library. | ||||||
|  |         Cursor folderCursor = database.query(TABLE_NAME_FOLDERS, | ||||||
|  |                 null,    // Get all columns. | ||||||
|  |                 null,    // Get all rows. | ||||||
|  |                 null, | ||||||
|  |                 null,    // No grouping. | ||||||
|  |                 null, | ||||||
|  |                 null);    // Order of folders is irrelevant. | ||||||
|  | 
 | ||||||
|  |         Set<String> allowedExtensions = new HashSet<String>(Arrays.asList( | ||||||
|  |                 ".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app", ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz")); | ||||||
|  | 
 | ||||||
|  |         // Possibly overly defensive, but ensures that moveToNext() does not skip a row. | ||||||
|  |         folderCursor.moveToPosition(-1); | ||||||
|  | 
 | ||||||
|  |         // Iterate through all results of the DB query (i.e. all folders in the library.) | ||||||
|  |         while (folderCursor.moveToNext()) { | ||||||
|  |             String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH); | ||||||
|  | 
 | ||||||
|  |             File folder = new File(folderPath); | ||||||
|  |             // If the folder is empty because it no longer exists, remove it from the library. | ||||||
|  |             if (!folder.exists()) { | ||||||
|  |                 Log.error( | ||||||
|  |                         "[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath); | ||||||
|  |                 database.delete(TABLE_NAME_FOLDERS, | ||||||
|  |                         KEY_DB_ID + " = ?", | ||||||
|  |                         new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))}); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             addGamesRecursive(database, folder, allowedExtensions, 3); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         fileCursor.close(); | ||||||
|  |         folderCursor.close(); | ||||||
|  | 
 | ||||||
|  |         Arrays.stream(NativeLibrary.GetInstalledGamePaths()) | ||||||
|  |                 .forEach(filePath -> attemptToAddGame(database, filePath)); | ||||||
|  | 
 | ||||||
|  |         database.close(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void addGamesRecursive(SQLiteDatabase database, File parent, Set<String> allowedExtensions, int depth) { | ||||||
|  |         if (depth <= 0) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         File[] children = parent.listFiles(); | ||||||
|  |         if (children != null) { | ||||||
|  |             for (File file : children) { | ||||||
|  |                 if (file.isHidden()) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (file.isDirectory()) { | ||||||
|  |                     Set<String> newExtensions = new HashSet<>(Arrays.asList( | ||||||
|  |                             ".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app")); | ||||||
|  |                     addGamesRecursive(database, file, newExtensions, depth - 1); | ||||||
|  |                 } else { | ||||||
|  |                     String filePath = file.getPath(); | ||||||
|  | 
 | ||||||
|  |                     int extensionStart = filePath.lastIndexOf('.'); | ||||||
|  |                     if (extensionStart > 0) { | ||||||
|  |                         String fileExtension = filePath.substring(extensionStart); | ||||||
|  | 
 | ||||||
|  |                         // Check that the file has an extension we care about before trying to read out of it. | ||||||
|  |                         if (allowedExtensions.contains(fileExtension.toLowerCase())) { | ||||||
|  |                             attemptToAddGame(database, filePath); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void attemptToAddGame(SQLiteDatabase database, String filePath) { | ||||||
|  |         String name = NativeLibrary.GetTitle(filePath); | ||||||
|  | 
 | ||||||
|  |         // If the game's title field is empty, use the filename. | ||||||
|  |         if (name.isEmpty()) { | ||||||
|  |             name = filePath.substring(filePath.lastIndexOf("/") + 1); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         String gameId = NativeLibrary.GetGameId(filePath); | ||||||
|  | 
 | ||||||
|  |         // If the game's ID field is empty, use the filename without extension. | ||||||
|  |         if (gameId.isEmpty()) { | ||||||
|  |             gameId = filePath.substring(filePath.lastIndexOf("/") + 1, | ||||||
|  |                     filePath.lastIndexOf(".")); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         ContentValues game = Game.asContentValues(name, | ||||||
|  |                 NativeLibrary.GetDescription(filePath).replace("\n", " "), | ||||||
|  |                 NativeLibrary.GetRegions(filePath), | ||||||
|  |                 filePath, | ||||||
|  |                 gameId, | ||||||
|  |                 NativeLibrary.GetCompany(filePath)); | ||||||
|  | 
 | ||||||
|  |         // Try to update an existing game first. | ||||||
|  |         int rowsMatched = database.update(TABLE_NAME_GAMES,    // Which table to update. | ||||||
|  |                 game, | ||||||
|  |                 // The values to fill the row with. | ||||||
|  |                 KEY_GAME_ID + " = ?", | ||||||
|  |                 // The WHERE clause used to find the right row. | ||||||
|  |                 new String[]{game.getAsString( | ||||||
|  |                         KEY_GAME_ID)});    // The ? in WHERE clause is replaced with this, | ||||||
|  |         // which is provided as an array because there | ||||||
|  |         // could potentially be more than one argument. | ||||||
|  | 
 | ||||||
|  |         // If update fails, insert a new game instead. | ||||||
|  |         if (rowsMatched == 0) { | ||||||
|  |             Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE)); | ||||||
|  |             database.insert(TABLE_NAME_GAMES, null, game); | ||||||
|  |         } else { | ||||||
|  |             Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Observable<Cursor> getGames() { | ||||||
|  |         return Observable.create(subscriber -> | ||||||
|  |         { | ||||||
|  |             Log.info("[GameDatabase] Reading games list..."); | ||||||
|  | 
 | ||||||
|  |             SQLiteDatabase database = getReadableDatabase(); | ||||||
|  |             Cursor resultCursor = database.query( | ||||||
|  |                     TABLE_NAME_GAMES, | ||||||
|  |                     null, | ||||||
|  |                     null, | ||||||
|  |                     null, | ||||||
|  |                     null, | ||||||
|  |                     null, | ||||||
|  |                     KEY_GAME_TITLE + " ASC" | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             // Pass the result cursor to the consumer. | ||||||
|  |             subscriber.onNext(resultCursor); | ||||||
|  | 
 | ||||||
|  |             // Tell the consumer we're done; it will unsubscribe implicitly. | ||||||
|  |             subscriber.onCompleted(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void execSqlAndLog(SQLiteDatabase database, String sql) { | ||||||
|  |         Log.verbose("[GameDatabase] Executing SQL: " + sql); | ||||||
|  |         database.execSQL(sql); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,138 @@ | ||||||
|  | package org.citra.citra_emu.model; | ||||||
|  | 
 | ||||||
|  | import android.content.ContentProvider; | ||||||
|  | import android.content.ContentValues; | ||||||
|  | import android.database.Cursor; | ||||||
|  | import android.database.sqlite.SQLiteDatabase; | ||||||
|  | import android.net.Uri; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.BuildConfig; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Provides an interface allowing Activities to interact with the SQLite database. | ||||||
|  |  * CRUD methods in this class can be called by Activities using getContentResolver(). | ||||||
|  |  */ | ||||||
|  | public final class GameProvider extends ContentProvider { | ||||||
|  |     public static final String REFRESH_LIBRARY = "refresh"; | ||||||
|  |     public static final String RESET_LIBRARY = "reset"; | ||||||
|  | 
 | ||||||
|  |     public static final String AUTHORITY = "content://" + BuildConfig.APPLICATION_ID + ".provider"; | ||||||
|  |     public static final Uri URI_FOLDER = | ||||||
|  |             Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_FOLDERS + "/"); | ||||||
|  |     public static final Uri URI_REFRESH = Uri.parse(AUTHORITY + "/" + REFRESH_LIBRARY + "/"); | ||||||
|  |     public static final Uri URI_RESET = Uri.parse(AUTHORITY + "/" + RESET_LIBRARY + "/"); | ||||||
|  | 
 | ||||||
|  |     public static final String MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.dolphin.folder"; | ||||||
|  |     public static final String MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.dolphin.game"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     private GameDatabase mDbHelper; | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onCreate() { | ||||||
|  |         Log.info("[GameProvider] Creating Content Provider..."); | ||||||
|  | 
 | ||||||
|  |         mDbHelper = new GameDatabase(getContext()); | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Cursor query(@NonNull Uri uri, String[] projection, String selection, | ||||||
|  |                         String[] selectionArgs, String sortOrder) { | ||||||
|  |         Log.info("[GameProvider] Querying URI: " + uri); | ||||||
|  | 
 | ||||||
|  |         SQLiteDatabase db = mDbHelper.getReadableDatabase(); | ||||||
|  | 
 | ||||||
|  |         String table = uri.getLastPathSegment(); | ||||||
|  | 
 | ||||||
|  |         if (table == null) { | ||||||
|  |             Log.error("[GameProvider] Badly formatted URI: " + uri); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder); | ||||||
|  |         cursor.setNotificationUri(getContext().getContentResolver(), uri); | ||||||
|  | 
 | ||||||
|  |         return cursor; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String getType(@NonNull Uri uri) { | ||||||
|  |         Log.verbose("[GameProvider] Getting MIME type for URI: " + uri); | ||||||
|  |         String lastSegment = uri.getLastPathSegment(); | ||||||
|  | 
 | ||||||
|  |         if (lastSegment == null) { | ||||||
|  |             Log.error("[GameProvider] Badly formatted URI: " + uri); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (lastSegment.equals(GameDatabase.TABLE_NAME_FOLDERS)) { | ||||||
|  |             return MIME_TYPE_FOLDER; | ||||||
|  |         } else if (lastSegment.equals(GameDatabase.TABLE_NAME_GAMES)) { | ||||||
|  |             return MIME_TYPE_GAME; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Log.error("[GameProvider] Unknown MIME type for URI: " + uri); | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Uri insert(@NonNull Uri uri, ContentValues values) { | ||||||
|  |         Log.info("[GameProvider] Inserting row at URI: " + uri); | ||||||
|  | 
 | ||||||
|  |         SQLiteDatabase database = mDbHelper.getWritableDatabase(); | ||||||
|  |         String table = uri.getLastPathSegment(); | ||||||
|  | 
 | ||||||
|  |         if (table != null) { | ||||||
|  |             if (table.equals(RESET_LIBRARY)) { | ||||||
|  |                 mDbHelper.resetDatabase(database); | ||||||
|  |                 return uri; | ||||||
|  |             } | ||||||
|  |             if (table.equals(REFRESH_LIBRARY)) { | ||||||
|  |                 Log.info( | ||||||
|  |                         "[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents..."); | ||||||
|  |                 mDbHelper.scanLibrary(database); | ||||||
|  |                 return uri; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             long id = database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE); | ||||||
|  | 
 | ||||||
|  |             // If insertion was successful... | ||||||
|  |             if (id > 0) { | ||||||
|  |                 // If we just added a folder, add its contents to the game list. | ||||||
|  |                 if (table.equals(GameDatabase.TABLE_NAME_FOLDERS)) { | ||||||
|  |                     mDbHelper.scanLibrary(database); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Notify the UI that its contents should be refreshed. | ||||||
|  |                 getContext().getContentResolver().notifyChange(uri, null); | ||||||
|  |                 uri = Uri.withAppendedPath(uri, Long.toString(id)); | ||||||
|  |             } else { | ||||||
|  |                 Log.error("[GameProvider] Row already exists: " + uri + " id: " + id); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             Log.error("[GameProvider] Badly formatted URI: " + uri); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         database.close(); | ||||||
|  | 
 | ||||||
|  |         return uri; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { | ||||||
|  |         Log.error("[GameProvider] Delete operations unsupported. URI: " + uri); | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int update(@NonNull Uri uri, ContentValues values, String selection, | ||||||
|  |                       String[] selectionArgs) { | ||||||
|  |         Log.error("[GameProvider] Update operations unsupported. URI: " + uri); | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,878 @@ | ||||||
|  | /** | ||||||
|  |  * Copyright 2013 Dolphin Emulator Project | ||||||
|  |  * Licensed under GPLv2+ | ||||||
|  |  * Refer to the license.txt file included. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.overlay; | ||||||
|  | 
 | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.content.res.Configuration; | ||||||
|  | import android.content.res.Resources; | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | import android.graphics.BitmapFactory; | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.graphics.Rect; | ||||||
|  | import android.graphics.drawable.Drawable; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | import android.util.AttributeSet; | ||||||
|  | import android.util.DisplayMetrics; | ||||||
|  | import android.view.Display; | ||||||
|  | import android.view.MotionEvent; | ||||||
|  | import android.view.SurfaceView; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.View.OnTouchListener; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.NativeLibrary.ButtonState; | ||||||
|  | import org.citra.citra_emu.NativeLibrary.ButtonType; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.utils.EmulationMenuSettings; | ||||||
|  | 
 | ||||||
|  | import java.util.HashSet; | ||||||
|  | import java.util.Set; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Draws the interactive input overlay on top of the | ||||||
|  |  * {@link SurfaceView} that is rendering emulation. | ||||||
|  |  */ | ||||||
|  | public final class InputOverlay extends SurfaceView implements OnTouchListener { | ||||||
|  |     private final Set<InputOverlayDrawableButton> overlayButtons = new HashSet<>(); | ||||||
|  |     private final Set<InputOverlayDrawableDpad> overlayDpads = new HashSet<>(); | ||||||
|  |     private final Set<InputOverlayDrawableJoystick> overlayJoysticks = new HashSet<>(); | ||||||
|  | 
 | ||||||
|  |     private boolean mIsInEditMode = false; | ||||||
|  |     private InputOverlayDrawableButton mButtonBeingConfigured; | ||||||
|  |     private InputOverlayDrawableDpad mDpadBeingConfigured; | ||||||
|  |     private InputOverlayDrawableJoystick mJoystickBeingConfigured; | ||||||
|  | 
 | ||||||
|  |     private SharedPreferences mPreferences; | ||||||
|  | 
 | ||||||
|  |     // Stores the ID of the pointer that interacted with the 3DS touchscreen. | ||||||
|  |     private int mTouchscreenPointerId = -1; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constructor | ||||||
|  |      * | ||||||
|  |      * @param context The current {@link Context}. | ||||||
|  |      * @param attrs   {@link AttributeSet} for parsing XML attributes. | ||||||
|  |      */ | ||||||
|  |     public InputOverlay(Context context, AttributeSet attrs) { | ||||||
|  |         super(context, attrs); | ||||||
|  | 
 | ||||||
|  |         mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); | ||||||
|  |         if (!mPreferences.getBoolean("OverlayInit", false)) { | ||||||
|  |             defaultOverlay(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Reset 3ds touchscreen pointer ID | ||||||
|  |         mTouchscreenPointerId = -1; | ||||||
|  | 
 | ||||||
|  |         // Load the controls. | ||||||
|  |         refreshControls(); | ||||||
|  | 
 | ||||||
|  |         // Set the on touch listener. | ||||||
|  |         setOnTouchListener(this); | ||||||
|  | 
 | ||||||
|  |         // Force draw | ||||||
|  |         setWillNotDraw(false); | ||||||
|  | 
 | ||||||
|  |         // Request focus for the overlay so it has priority on presses. | ||||||
|  |         requestFocus(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Resizes a {@link Bitmap} by a given scale factor | ||||||
|  |      * | ||||||
|  |      * @param context The current {@link Context} | ||||||
|  |      * @param bitmap  The {@link Bitmap} to scale. | ||||||
|  |      * @param scale   The scale factor for the bitmap. | ||||||
|  |      * @return The scaled {@link Bitmap} | ||||||
|  |      */ | ||||||
|  |     public static Bitmap resizeBitmap(Context context, Bitmap bitmap, float scale) { | ||||||
|  |         // Determine the button size based on the smaller screen dimension. | ||||||
|  |         // This makes sure the buttons are the same size in both portrait and landscape. | ||||||
|  |         DisplayMetrics dm = context.getResources().getDisplayMetrics(); | ||||||
|  |         int minDimension = Math.min(dm.widthPixels, dm.heightPixels); | ||||||
|  | 
 | ||||||
|  |         return Bitmap.createScaledBitmap(bitmap, | ||||||
|  |                 (int) (minDimension * scale), | ||||||
|  |                 (int) (minDimension * scale), | ||||||
|  |                 true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initializes an InputOverlayDrawableButton, given by resId, with all of the | ||||||
|  |      * parameters set for it to be properly shown on the InputOverlay. | ||||||
|  |      * <p> | ||||||
|  |      * This works due to the way the X and Y coordinates are stored within | ||||||
|  |      * the {@link SharedPreferences}. | ||||||
|  |      * <p> | ||||||
|  |      * In the input overlay configuration menu, | ||||||
|  |      * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay). | ||||||
|  |      * the X and Y coordinates of the button at the END of its touch event | ||||||
|  |      * (when you remove your finger/stylus from the touchscreen) are then stored | ||||||
|  |      * within a SharedPreferences instance so that those values can be retrieved here. | ||||||
|  |      * <p> | ||||||
|  |      * This has a few benefits over the conventional way of storing the values | ||||||
|  |      * (ie. within the Citra ini file). | ||||||
|  |      * <ul> | ||||||
|  |      * <li>No native calls</li> | ||||||
|  |      * <li>Keeps Android-only values inside the Android environment</li> | ||||||
|  |      * </ul> | ||||||
|  |      * <p> | ||||||
|  |      * Technically no modifications should need to be performed on the returned | ||||||
|  |      * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait | ||||||
|  |      * for Android to call the onDraw method. | ||||||
|  |      * | ||||||
|  |      * @param context      The current {@link Context}. | ||||||
|  |      * @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State). | ||||||
|  |      * @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State). | ||||||
|  |      * @param buttonId     Identifier for determining what type of button the initialized InputOverlayDrawableButton represents. | ||||||
|  |      * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set. | ||||||
|  |      */ | ||||||
|  |     private static InputOverlayDrawableButton initializeOverlayButton(Context context, | ||||||
|  |                                                                       int defaultResId, int pressedResId, int buttonId, String orientation) { | ||||||
|  |         // Resources handle for fetching the initial Drawable resource. | ||||||
|  |         final Resources res = context.getResources(); | ||||||
|  | 
 | ||||||
|  |         // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. | ||||||
|  |         final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|  | 
 | ||||||
|  |         // Decide scale based on button ID and user preference | ||||||
|  |         float scale; | ||||||
|  | 
 | ||||||
|  |         switch (buttonId) { | ||||||
|  |             case ButtonType.BUTTON_HOME: | ||||||
|  |             case ButtonType.BUTTON_START: | ||||||
|  |             case ButtonType.BUTTON_SELECT: | ||||||
|  |                 scale = 0.08f; | ||||||
|  |                 break; | ||||||
|  |             case ButtonType.TRIGGER_L: | ||||||
|  |             case ButtonType.TRIGGER_R: | ||||||
|  |             case ButtonType.BUTTON_ZL: | ||||||
|  |             case ButtonType.BUTTON_ZR: | ||||||
|  |                 scale = 0.18f; | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 scale = 0.11f; | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         scale *= (sPrefs.getInt("controlScale", 50) + 50); | ||||||
|  |         scale /= 100; | ||||||
|  | 
 | ||||||
|  |         // Initialize the InputOverlayDrawableButton. | ||||||
|  |         final Bitmap defaultStateBitmap = | ||||||
|  |                 resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); | ||||||
|  |         final Bitmap pressedStateBitmap = | ||||||
|  |                 resizeBitmap(context, BitmapFactory.decodeResource(res, pressedResId), scale); | ||||||
|  |         final InputOverlayDrawableButton overlayDrawable = | ||||||
|  |                 new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId); | ||||||
|  | 
 | ||||||
|  |         // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. | ||||||
|  |         // These were set in the input overlay configuration menu. | ||||||
|  |         String xKey; | ||||||
|  |         String yKey; | ||||||
|  | 
 | ||||||
|  |         xKey = buttonId + orientation + "-X"; | ||||||
|  |         yKey = buttonId + orientation + "-Y"; | ||||||
|  | 
 | ||||||
|  |         int drawableX = (int) sPrefs.getFloat(xKey, 0f); | ||||||
|  |         int drawableY = (int) sPrefs.getFloat(yKey, 0f); | ||||||
|  | 
 | ||||||
|  |         int width = overlayDrawable.getWidth(); | ||||||
|  |         int height = overlayDrawable.getHeight(); | ||||||
|  | 
 | ||||||
|  |         // Now set the bounds for the InputOverlayDrawableButton. | ||||||
|  |         // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. | ||||||
|  |         overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); | ||||||
|  | 
 | ||||||
|  |         // Need to set the image's position | ||||||
|  |         overlayDrawable.setPosition(drawableX, drawableY); | ||||||
|  | 
 | ||||||
|  |         return overlayDrawable; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initializes an {@link InputOverlayDrawableDpad} | ||||||
|  |      * | ||||||
|  |      * @param context                   The current {@link Context}. | ||||||
|  |      * @param defaultResId              The {@link Bitmap} resource ID of the default sate. | ||||||
|  |      * @param pressedOneDirectionResId  The {@link Bitmap} resource ID of the pressed sate in one direction. | ||||||
|  |      * @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions. | ||||||
|  |      * @param buttonUp                  Identifier for the up button. | ||||||
|  |      * @param buttonDown                Identifier for the down button. | ||||||
|  |      * @param buttonLeft                Identifier for the left button. | ||||||
|  |      * @param buttonRight               Identifier for the right button. | ||||||
|  |      * @return the initialized {@link InputOverlayDrawableDpad} | ||||||
|  |      */ | ||||||
|  |     private static InputOverlayDrawableDpad initializeOverlayDpad(Context context, | ||||||
|  |                                                                   int defaultResId, | ||||||
|  |                                                                   int pressedOneDirectionResId, | ||||||
|  |                                                                   int pressedTwoDirectionsResId, | ||||||
|  |                                                                   int buttonUp, | ||||||
|  |                                                                   int buttonDown, | ||||||
|  |                                                                   int buttonLeft, | ||||||
|  |                                                                   int buttonRight, | ||||||
|  |                                                                   String orientation) { | ||||||
|  |         // Resources handle for fetching the initial Drawable resource. | ||||||
|  |         final Resources res = context.getResources(); | ||||||
|  | 
 | ||||||
|  |         // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad. | ||||||
|  |         final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|  | 
 | ||||||
|  |         // Decide scale based on button ID and user preference | ||||||
|  |         float scale = 0.22f; | ||||||
|  | 
 | ||||||
|  |         scale *= (sPrefs.getInt("controlScale", 50) + 50); | ||||||
|  |         scale /= 100; | ||||||
|  | 
 | ||||||
|  |         // Initialize the InputOverlayDrawableDpad. | ||||||
|  |         final Bitmap defaultStateBitmap = | ||||||
|  |                 resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); | ||||||
|  |         final Bitmap pressedOneDirectionStateBitmap = | ||||||
|  |                 resizeBitmap(context, BitmapFactory.decodeResource(res, pressedOneDirectionResId), | ||||||
|  |                         scale); | ||||||
|  |         final Bitmap pressedTwoDirectionsStateBitmap = | ||||||
|  |                 resizeBitmap(context, BitmapFactory.decodeResource(res, pressedTwoDirectionsResId), | ||||||
|  |                         scale); | ||||||
|  |         final InputOverlayDrawableDpad overlayDrawable = | ||||||
|  |                 new InputOverlayDrawableDpad(res, defaultStateBitmap, | ||||||
|  |                         pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap, | ||||||
|  |                         buttonUp, buttonDown, buttonLeft, buttonRight); | ||||||
|  | 
 | ||||||
|  |         // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. | ||||||
|  |         // These were set in the input overlay configuration menu. | ||||||
|  |         int drawableX = (int) sPrefs.getFloat(buttonUp + orientation + "-X", 0f); | ||||||
|  |         int drawableY = (int) sPrefs.getFloat(buttonUp + orientation + "-Y", 0f); | ||||||
|  | 
 | ||||||
|  |         int width = overlayDrawable.getWidth(); | ||||||
|  |         int height = overlayDrawable.getHeight(); | ||||||
|  | 
 | ||||||
|  |         // Now set the bounds for the InputOverlayDrawableDpad. | ||||||
|  |         // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be. | ||||||
|  |         overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); | ||||||
|  | 
 | ||||||
|  |         // Need to set the image's position | ||||||
|  |         overlayDrawable.setPosition(drawableX, drawableY); | ||||||
|  | 
 | ||||||
|  |         return overlayDrawable; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initializes an {@link InputOverlayDrawableJoystick} | ||||||
|  |      * | ||||||
|  |      * @param context         The current {@link Context} | ||||||
|  |      * @param resOuter        Resource ID for the outer image of the joystick (the static image that shows the circular bounds). | ||||||
|  |      * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around). | ||||||
|  |      * @param pressedResInner Resource ID for the pressed inner image of the joystick. | ||||||
|  |      * @param joystick        Identifier for which joystick this is. | ||||||
|  |      * @return the initialized {@link InputOverlayDrawableJoystick}. | ||||||
|  |      */ | ||||||
|  |     private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, | ||||||
|  |                                                                           int resOuter, int defaultResInner, int pressedResInner, int joystick, String orientation) { | ||||||
|  |         // Resources handle for fetching the initial Drawable resource. | ||||||
|  |         final Resources res = context.getResources(); | ||||||
|  | 
 | ||||||
|  |         // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick. | ||||||
|  |         final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|  | 
 | ||||||
|  |         // Decide scale based on user preference | ||||||
|  |         float scale = 0.275f; | ||||||
|  |         scale *= (sPrefs.getInt("controlScale", 50) + 50); | ||||||
|  |         scale /= 100; | ||||||
|  | 
 | ||||||
|  |         // Initialize the InputOverlayDrawableJoystick. | ||||||
|  |         final Bitmap bitmapOuter = | ||||||
|  |                 resizeBitmap(context, BitmapFactory.decodeResource(res, resOuter), scale); | ||||||
|  |         final Bitmap bitmapInnerDefault = BitmapFactory.decodeResource(res, defaultResInner); | ||||||
|  |         final Bitmap bitmapInnerPressed = BitmapFactory.decodeResource(res, pressedResInner); | ||||||
|  | 
 | ||||||
|  |         // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. | ||||||
|  |         // These were set in the input overlay configuration menu. | ||||||
|  |         int drawableX = (int) sPrefs.getFloat(joystick + orientation + "-X", 0f); | ||||||
|  |         int drawableY = (int) sPrefs.getFloat(joystick + orientation + "-Y", 0f); | ||||||
|  | 
 | ||||||
|  |         // Decide inner scale based on joystick ID | ||||||
|  |         float outerScale = 1.f; | ||||||
|  |         if (joystick == ButtonType.STICK_C) { | ||||||
|  |             outerScale = 2.f; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Now set the bounds for the InputOverlayDrawableJoystick. | ||||||
|  |         // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. | ||||||
|  |         int outerSize = bitmapOuter.getWidth(); | ||||||
|  |         Rect outerRect = new Rect(drawableX, drawableY, drawableX + (int) (outerSize / outerScale), drawableY + (int) (outerSize / outerScale)); | ||||||
|  |         Rect innerRect = new Rect(0, 0, (int) (outerSize / outerScale), (int) (outerSize / outerScale)); | ||||||
|  | 
 | ||||||
|  |         // Send the drawableId to the joystick so it can be referenced when saving control position. | ||||||
|  |         final InputOverlayDrawableJoystick overlayDrawable | ||||||
|  |                 = new InputOverlayDrawableJoystick(res, bitmapOuter, | ||||||
|  |                 bitmapInnerDefault, bitmapInnerPressed, | ||||||
|  |                 outerRect, innerRect, joystick); | ||||||
|  | 
 | ||||||
|  |         // Need to set the image's position | ||||||
|  |         overlayDrawable.setPosition(drawableX, drawableY); | ||||||
|  | 
 | ||||||
|  |         return overlayDrawable; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void draw(Canvas canvas) { | ||||||
|  |         super.draw(canvas); | ||||||
|  | 
 | ||||||
|  |         for (InputOverlayDrawableButton button : overlayButtons) { | ||||||
|  |             button.draw(canvas); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (InputOverlayDrawableDpad dpad : overlayDpads) { | ||||||
|  |             dpad.draw(canvas); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { | ||||||
|  |             joystick.draw(canvas); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onTouch(View v, MotionEvent event) { | ||||||
|  |         if (isInEditMode()) { | ||||||
|  |             return onTouchWhileEditing(event); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         int pointerIndex = event.getActionIndex(); | ||||||
|  | 
 | ||||||
|  |         if (mPreferences.getBoolean("isTouchEnabled", true)) { | ||||||
|  |             switch (event.getAction() & MotionEvent.ACTION_MASK) { | ||||||
|  |                 case MotionEvent.ACTION_DOWN: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_DOWN: | ||||||
|  |                     if (NativeLibrary.onTouchEvent(event.getX(pointerIndex), event.getY(pointerIndex), true)) { | ||||||
|  |                         mTouchscreenPointerId = event.getPointerId(pointerIndex); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 case MotionEvent.ACTION_UP: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_UP: | ||||||
|  |                     if (mTouchscreenPointerId == event.getPointerId(pointerIndex)) { | ||||||
|  |                         // We don't really care where the touch has been released. We only care whether it has been | ||||||
|  |                         // released or not. | ||||||
|  |                         NativeLibrary.onTouchEvent(0, 0, false); | ||||||
|  |                         mTouchscreenPointerId = -1; | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             for (int i = 0; i < event.getPointerCount(); i++) { | ||||||
|  |                 if (mTouchscreenPointerId == event.getPointerId(i)) { | ||||||
|  |                     NativeLibrary.onTouchMoved(event.getX(i), event.getY(i)); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (InputOverlayDrawableButton button : overlayButtons) { | ||||||
|  |             // Determine the button state to apply based on the MotionEvent action flag. | ||||||
|  |             switch (event.getAction() & MotionEvent.ACTION_MASK) { | ||||||
|  |                 case MotionEvent.ACTION_DOWN: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_DOWN: | ||||||
|  |                     // If a pointer enters the bounds of a button, press that button. | ||||||
|  |                     if (button.getBounds() | ||||||
|  |                             .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { | ||||||
|  |                         button.setPressedState(true); | ||||||
|  |                         button.setTrackId(event.getPointerId(pointerIndex)); | ||||||
|  |                         NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), | ||||||
|  |                                 ButtonState.PRESSED); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 case MotionEvent.ACTION_UP: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_UP: | ||||||
|  |                     // If a pointer ends, release the button it was pressing. | ||||||
|  |                     if (button.getTrackId() == event.getPointerId(pointerIndex)) { | ||||||
|  |                         button.setPressedState(false); | ||||||
|  |                         NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), | ||||||
|  |                                 ButtonState.RELEASED); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (InputOverlayDrawableDpad dpad : overlayDpads) { | ||||||
|  |             // Determine the button state to apply based on the MotionEvent action flag. | ||||||
|  |             switch (event.getAction() & MotionEvent.ACTION_MASK) { | ||||||
|  |                 case MotionEvent.ACTION_DOWN: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_DOWN: | ||||||
|  |                     // If a pointer enters the bounds of a button, press that button. | ||||||
|  |                     if (dpad.getBounds() | ||||||
|  |                             .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { | ||||||
|  |                         dpad.setTrackId(event.getPointerId(pointerIndex)); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 case MotionEvent.ACTION_UP: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_UP: | ||||||
|  |                     // If a pointer ends, release the buttons. | ||||||
|  |                     if (dpad.getTrackId() == event.getPointerId(pointerIndex)) { | ||||||
|  |                         for (int i = 0; i < 4; i++) { | ||||||
|  |                             dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT); | ||||||
|  |                             NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(i), | ||||||
|  |                                     NativeLibrary.ButtonState.RELEASED); | ||||||
|  |                         } | ||||||
|  |                         dpad.setTrackId(-1); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (dpad.getTrackId() != -1) { | ||||||
|  |                 for (int i = 0; i < event.getPointerCount(); i++) { | ||||||
|  |                     if (dpad.getTrackId() == event.getPointerId(i)) { | ||||||
|  |                         float touchX = event.getX(i); | ||||||
|  |                         float touchY = event.getY(i); | ||||||
|  |                         float maxY = dpad.getBounds().bottom; | ||||||
|  |                         float maxX = dpad.getBounds().right; | ||||||
|  |                         touchX -= dpad.getBounds().centerX(); | ||||||
|  |                         maxX -= dpad.getBounds().centerX(); | ||||||
|  |                         touchY -= dpad.getBounds().centerY(); | ||||||
|  |                         maxY -= dpad.getBounds().centerY(); | ||||||
|  |                         final float AxisX = touchX / maxX; | ||||||
|  |                         final float AxisY = touchY / maxY; | ||||||
|  | 
 | ||||||
|  |                         boolean up = false; | ||||||
|  |                         boolean down = false; | ||||||
|  |                         boolean left = false; | ||||||
|  |                         boolean right = false; | ||||||
|  |                         if (EmulationMenuSettings.getDpadSlideEnable() || | ||||||
|  |                                 (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN || | ||||||
|  |                                 (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { | ||||||
|  |                             if (AxisY < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { | ||||||
|  |                                 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0), | ||||||
|  |                                         NativeLibrary.ButtonState.PRESSED); | ||||||
|  |                                 up = true; | ||||||
|  |                             } else { | ||||||
|  |                                 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0), | ||||||
|  |                                         NativeLibrary.ButtonState.RELEASED); | ||||||
|  |                             } | ||||||
|  |                             if (AxisY > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { | ||||||
|  |                                 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1), | ||||||
|  |                                         NativeLibrary.ButtonState.PRESSED); | ||||||
|  |                                 down = true; | ||||||
|  |                             } else { | ||||||
|  |                                 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1), | ||||||
|  |                                         NativeLibrary.ButtonState.RELEASED); | ||||||
|  |                             } | ||||||
|  |                             if (AxisX < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { | ||||||
|  |                                 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2), | ||||||
|  |                                         NativeLibrary.ButtonState.PRESSED); | ||||||
|  |                                 left = true; | ||||||
|  |                             } else { | ||||||
|  |                                 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2), | ||||||
|  |                                         NativeLibrary.ButtonState.RELEASED); | ||||||
|  |                             } | ||||||
|  |                             if (AxisX > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { | ||||||
|  |                                 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3), | ||||||
|  |                                         NativeLibrary.ButtonState.PRESSED); | ||||||
|  |                                 right = true; | ||||||
|  |                             } else { | ||||||
|  |                                 NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3), | ||||||
|  |                                         NativeLibrary.ButtonState.RELEASED); | ||||||
|  |                             } | ||||||
|  | 
 | ||||||
|  |                             // Set state | ||||||
|  |                             if (up) { | ||||||
|  |                                 if (left) | ||||||
|  |                                     dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT); | ||||||
|  |                                 else if (right) | ||||||
|  |                                     dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT); | ||||||
|  |                                 else | ||||||
|  |                                     dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP); | ||||||
|  |                             } else if (down) { | ||||||
|  |                                 if (left) | ||||||
|  |                                     dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT); | ||||||
|  |                                 else if (right) | ||||||
|  |                                     dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT); | ||||||
|  |                                 else | ||||||
|  |                                     dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN); | ||||||
|  |                             } else if (left) { | ||||||
|  |                                 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT); | ||||||
|  |                             } else if (right) { | ||||||
|  |                                 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT); | ||||||
|  |                             } else { | ||||||
|  |                                 dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { | ||||||
|  |             joystick.TrackEvent(event); | ||||||
|  |             int axisID = joystick.getId(); | ||||||
|  |             float[] axises = joystick.getAxisValues(); | ||||||
|  | 
 | ||||||
|  |             NativeLibrary | ||||||
|  |                     .onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisID, axises[0], axises[1]); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         invalidate(); | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean onTouchWhileEditing(MotionEvent event) { | ||||||
|  |         int pointerIndex = event.getActionIndex(); | ||||||
|  |         int fingerPositionX = (int) event.getX(pointerIndex); | ||||||
|  |         int fingerPositionY = (int) event.getY(pointerIndex); | ||||||
|  | 
 | ||||||
|  |         String orientation = | ||||||
|  |                 getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? | ||||||
|  |                         "-Portrait" : ""; | ||||||
|  | 
 | ||||||
|  |         // Maybe combine Button and Joystick as subclasses of the same parent? | ||||||
|  |         // Or maybe create an interface like IMoveableHUDControl? | ||||||
|  | 
 | ||||||
|  |         for (InputOverlayDrawableButton button : overlayButtons) { | ||||||
|  |             // Determine the button state to apply based on the MotionEvent action flag. | ||||||
|  |             switch (event.getAction() & MotionEvent.ACTION_MASK) { | ||||||
|  |                 case MotionEvent.ACTION_DOWN: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_DOWN: | ||||||
|  |                     // If no button is being moved now, remember the currently touched button to move. | ||||||
|  |                     if (mButtonBeingConfigured == null && | ||||||
|  |                             button.getBounds().contains(fingerPositionX, fingerPositionY)) { | ||||||
|  |                         mButtonBeingConfigured = button; | ||||||
|  |                         mButtonBeingConfigured.onConfigureTouch(event); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 case MotionEvent.ACTION_MOVE: | ||||||
|  |                     if (mButtonBeingConfigured != null) { | ||||||
|  |                         mButtonBeingConfigured.onConfigureTouch(event); | ||||||
|  |                         invalidate(); | ||||||
|  |                         return true; | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  | 
 | ||||||
|  |                 case MotionEvent.ACTION_UP: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_UP: | ||||||
|  |                     if (mButtonBeingConfigured == button) { | ||||||
|  |                         // Persist button position by saving new place. | ||||||
|  |                         saveControlPosition(mButtonBeingConfigured.getId(), | ||||||
|  |                                 mButtonBeingConfigured.getBounds().left, | ||||||
|  |                                 mButtonBeingConfigured.getBounds().top, orientation); | ||||||
|  |                         mButtonBeingConfigured = null; | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (InputOverlayDrawableDpad dpad : overlayDpads) { | ||||||
|  |             // Determine the button state to apply based on the MotionEvent action flag. | ||||||
|  |             switch (event.getAction() & MotionEvent.ACTION_MASK) { | ||||||
|  |                 case MotionEvent.ACTION_DOWN: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_DOWN: | ||||||
|  |                     // If no button is being moved now, remember the currently touched button to move. | ||||||
|  |                     if (mButtonBeingConfigured == null && | ||||||
|  |                             dpad.getBounds().contains(fingerPositionX, fingerPositionY)) { | ||||||
|  |                         mDpadBeingConfigured = dpad; | ||||||
|  |                         mDpadBeingConfigured.onConfigureTouch(event); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 case MotionEvent.ACTION_MOVE: | ||||||
|  |                     if (mDpadBeingConfigured != null) { | ||||||
|  |                         mDpadBeingConfigured.onConfigureTouch(event); | ||||||
|  |                         invalidate(); | ||||||
|  |                         return true; | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  | 
 | ||||||
|  |                 case MotionEvent.ACTION_UP: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_UP: | ||||||
|  |                     if (mDpadBeingConfigured == dpad) { | ||||||
|  |                         // Persist button position by saving new place. | ||||||
|  |                         saveControlPosition(mDpadBeingConfigured.getId(0), | ||||||
|  |                                 mDpadBeingConfigured.getBounds().left, mDpadBeingConfigured.getBounds().top, | ||||||
|  |                                 orientation); | ||||||
|  |                         mDpadBeingConfigured = null; | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { | ||||||
|  |             switch (event.getAction()) { | ||||||
|  |                 case MotionEvent.ACTION_DOWN: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_DOWN: | ||||||
|  |                     if (mJoystickBeingConfigured == null && | ||||||
|  |                             joystick.getBounds().contains(fingerPositionX, fingerPositionY)) { | ||||||
|  |                         mJoystickBeingConfigured = joystick; | ||||||
|  |                         mJoystickBeingConfigured.onConfigureTouch(event); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 case MotionEvent.ACTION_MOVE: | ||||||
|  |                     if (mJoystickBeingConfigured != null) { | ||||||
|  |                         mJoystickBeingConfigured.onConfigureTouch(event); | ||||||
|  |                         invalidate(); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 case MotionEvent.ACTION_UP: | ||||||
|  |                 case MotionEvent.ACTION_POINTER_UP: | ||||||
|  |                     if (mJoystickBeingConfigured != null) { | ||||||
|  |                         saveControlPosition(mJoystickBeingConfigured.getId(), | ||||||
|  |                                 mJoystickBeingConfigured.getBounds().left, | ||||||
|  |                                 mJoystickBeingConfigured.getBounds().top, orientation); | ||||||
|  |                         mJoystickBeingConfigured = null; | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setDpadState(InputOverlayDrawableDpad dpad, boolean up, boolean down, boolean left, | ||||||
|  |                               boolean right) { | ||||||
|  |         if (up) { | ||||||
|  |             if (left) | ||||||
|  |                 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT); | ||||||
|  |             else if (right) | ||||||
|  |                 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT); | ||||||
|  |             else | ||||||
|  |                 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP); | ||||||
|  |         } else if (down) { | ||||||
|  |             if (left) | ||||||
|  |                 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT); | ||||||
|  |             else if (right) | ||||||
|  |                 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT); | ||||||
|  |             else | ||||||
|  |                 dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN); | ||||||
|  |         } else if (left) { | ||||||
|  |             dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT); | ||||||
|  |         } else if (right) { | ||||||
|  |             dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void addOverlayControls(String orientation) { | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle0", true)) { | ||||||
|  |             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_a, | ||||||
|  |                     R.drawable.button_a_pressed, ButtonType.BUTTON_A, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle1", true)) { | ||||||
|  |             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_b, | ||||||
|  |                     R.drawable.button_b_pressed, ButtonType.BUTTON_B, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle2", true)) { | ||||||
|  |             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_x, | ||||||
|  |                     R.drawable.button_x_pressed, ButtonType.BUTTON_X, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle3", true)) { | ||||||
|  |             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_y, | ||||||
|  |                     R.drawable.button_y_pressed, ButtonType.BUTTON_Y, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle4", true)) { | ||||||
|  |             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l, | ||||||
|  |                     R.drawable.button_l_pressed, ButtonType.TRIGGER_L, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle5", true)) { | ||||||
|  |             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r, | ||||||
|  |                     R.drawable.button_r_pressed, ButtonType.TRIGGER_R, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle6", false)) { | ||||||
|  |             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zl, | ||||||
|  |                     R.drawable.button_zl_pressed, ButtonType.BUTTON_ZL, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle7", false)) { | ||||||
|  |             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zr, | ||||||
|  |                     R.drawable.button_zr_pressed, ButtonType.BUTTON_ZR, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle8", true)) { | ||||||
|  |             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_start, | ||||||
|  |                     R.drawable.button_start_pressed, ButtonType.BUTTON_START, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle9", true)) { | ||||||
|  |             overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_select, | ||||||
|  |                     R.drawable.button_select_pressed, ButtonType.BUTTON_SELECT, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle10", true)) { | ||||||
|  |             overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad, | ||||||
|  |                     R.drawable.dpad_pressed_one_direction, | ||||||
|  |                     R.drawable.dpad_pressed_two_directions, | ||||||
|  |                     ButtonType.DPAD_UP, ButtonType.DPAD_DOWN, | ||||||
|  |                     ButtonType.DPAD_LEFT, ButtonType.DPAD_RIGHT, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle11", true)) { | ||||||
|  |             overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_main_range, | ||||||
|  |                     R.drawable.stick_main, R.drawable.stick_main_pressed, | ||||||
|  |                     ButtonType.STICK_LEFT, orientation)); | ||||||
|  |         } | ||||||
|  |         if (mPreferences.getBoolean("buttonToggle12", false)) { | ||||||
|  |             overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_c_range, | ||||||
|  |                     R.drawable.stick_c, R.drawable.stick_c_pressed, ButtonType.STICK_C, orientation)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void refreshControls() { | ||||||
|  |         // Remove all the overlay buttons from the HashSet. | ||||||
|  |         overlayButtons.clear(); | ||||||
|  |         overlayDpads.clear(); | ||||||
|  |         overlayJoysticks.clear(); | ||||||
|  | 
 | ||||||
|  |         String orientation = | ||||||
|  |                 getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? | ||||||
|  |                         "-Portrait" : ""; | ||||||
|  | 
 | ||||||
|  |         // Add all the enabled overlay items back to the HashSet. | ||||||
|  |         if (EmulationMenuSettings.getShowOverlay()) { | ||||||
|  |             addOverlayControls(orientation); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         invalidate(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) { | ||||||
|  |         final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); | ||||||
|  |         SharedPreferences.Editor sPrefsEditor = sPrefs.edit(); | ||||||
|  |         sPrefsEditor.putFloat(sharedPrefsId + orientation + "-X", x); | ||||||
|  |         sPrefsEditor.putFloat(sharedPrefsId + orientation + "-Y", y); | ||||||
|  |         sPrefsEditor.apply(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setIsInEditMode(boolean isInEditMode) { | ||||||
|  |         mIsInEditMode = isInEditMode; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void defaultOverlay() { | ||||||
|  |         if (!mPreferences.getBoolean("OverlayInit", false)) { | ||||||
|  |             // It's possible that a user has created their overlay before this was added | ||||||
|  |             // Only change the overlay if the 'A' button is not in the upper corner. | ||||||
|  |             if (mPreferences.getFloat(ButtonType.BUTTON_A + "-X", 0f) == 0f) { | ||||||
|  |                 defaultOverlayLandscape(); | ||||||
|  |             } | ||||||
|  |             if (mPreferences.getFloat(ButtonType.BUTTON_A + "-Portrait" + "-X", 0f) == 0f) { | ||||||
|  |                 defaultOverlayPortrait(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); | ||||||
|  |         sPrefsEditor.putBoolean("OverlayInit", true); | ||||||
|  |         sPrefsEditor.apply(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void resetButtonPlacement() { | ||||||
|  |         boolean isLandscape = | ||||||
|  |                 getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; | ||||||
|  | 
 | ||||||
|  |         if (isLandscape) { | ||||||
|  |             defaultOverlayLandscape(); | ||||||
|  |         } else { | ||||||
|  |             defaultOverlayPortrait(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         refreshControls(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void defaultOverlayLandscape() { | ||||||
|  |         SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); | ||||||
|  |         // Get screen size | ||||||
|  |         Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); | ||||||
|  |         DisplayMetrics outMetrics = new DisplayMetrics(); | ||||||
|  |         display.getMetrics(outMetrics); | ||||||
|  |         float maxX = outMetrics.heightPixels; | ||||||
|  |         float maxY = outMetrics.widthPixels; | ||||||
|  |         // Height and width changes depending on orientation. Use the larger value for height. | ||||||
|  |         if (maxY > maxX) { | ||||||
|  |             float tmp = maxX; | ||||||
|  |             maxX = maxY; | ||||||
|  |             maxY = tmp; | ||||||
|  |         } | ||||||
|  |         Resources res = getResources(); | ||||||
|  | 
 | ||||||
|  |         // Each value is a percent from max X/Y stored as an int. Have to bring that value down | ||||||
|  |         // to a decimal before multiplying by MAX X/Y. | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.STICK_C + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.STICK_C + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_Y) / 1000) * maxY)); | ||||||
|  | 
 | ||||||
|  |         // We want to commit right away, otherwise the overlay could load before this is saved. | ||||||
|  |         sPrefsEditor.commit(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void defaultOverlayPortrait() { | ||||||
|  |         SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); | ||||||
|  |         // Get screen size | ||||||
|  |         Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); | ||||||
|  |         DisplayMetrics outMetrics = new DisplayMetrics(); | ||||||
|  |         display.getMetrics(outMetrics); | ||||||
|  |         float maxX = outMetrics.heightPixels; | ||||||
|  |         float maxY = outMetrics.widthPixels; | ||||||
|  |         // Height and width changes depending on orientation. Use the larger value for height. | ||||||
|  |         if (maxY < maxX) { | ||||||
|  |             float tmp = maxX; | ||||||
|  |             maxX = maxY; | ||||||
|  |             maxY = tmp; | ||||||
|  |         } | ||||||
|  |         Resources res = getResources(); | ||||||
|  |         String portrait = "-Portrait"; | ||||||
|  | 
 | ||||||
|  |         // Each value is a percent from max X/Y stored as an int. Have to bring that value down | ||||||
|  |         // to a decimal before multiplying by MAX X/Y. | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_X) / 1000) * maxX)); | ||||||
|  |         sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_Y) / 1000) * maxY)); | ||||||
|  | 
 | ||||||
|  |         // We want to commit right away, otherwise the overlay could load before this is saved. | ||||||
|  |         sPrefsEditor.commit(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isInEditMode() { | ||||||
|  |         return mIsInEditMode; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,122 @@ | ||||||
|  | /** | ||||||
|  |  * Copyright 2013 Dolphin Emulator Project | ||||||
|  |  * Licensed under GPLv2+ | ||||||
|  |  * Refer to the license.txt file included. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.overlay; | ||||||
|  | 
 | ||||||
|  | import android.content.res.Resources; | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.graphics.Rect; | ||||||
|  | import android.graphics.drawable.BitmapDrawable; | ||||||
|  | import android.view.MotionEvent; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Custom {@link BitmapDrawable} that is capable | ||||||
|  |  * of storing it's own ID. | ||||||
|  |  */ | ||||||
|  | public final class InputOverlayDrawableButton { | ||||||
|  |     // The ID identifying what type of button this Drawable represents. | ||||||
|  |     private int mButtonType; | ||||||
|  |     private int mTrackId; | ||||||
|  |     private int mPreviousTouchX, mPreviousTouchY; | ||||||
|  |     private int mControlPositionX, mControlPositionY; | ||||||
|  |     private int mWidth; | ||||||
|  |     private int mHeight; | ||||||
|  |     private BitmapDrawable mDefaultStateBitmap; | ||||||
|  |     private BitmapDrawable mPressedStateBitmap; | ||||||
|  |     private boolean mPressedState = false; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constructor | ||||||
|  |      * | ||||||
|  |      * @param res                {@link Resources} instance. | ||||||
|  |      * @param defaultStateBitmap {@link Bitmap} to use with the default state Drawable. | ||||||
|  |      * @param pressedStateBitmap {@link Bitmap} to use with the pressed state Drawable. | ||||||
|  |      * @param buttonType         Identifier for this type of button. | ||||||
|  |      */ | ||||||
|  |     public InputOverlayDrawableButton(Resources res, Bitmap defaultStateBitmap, | ||||||
|  |                                       Bitmap pressedStateBitmap, int buttonType) { | ||||||
|  |         mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); | ||||||
|  |         mPressedStateBitmap = new BitmapDrawable(res, pressedStateBitmap); | ||||||
|  |         mButtonType = buttonType; | ||||||
|  | 
 | ||||||
|  |         mWidth = mDefaultStateBitmap.getIntrinsicWidth(); | ||||||
|  |         mHeight = mDefaultStateBitmap.getIntrinsicHeight(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets this InputOverlayDrawableButton's button ID. | ||||||
|  |      * | ||||||
|  |      * @return this InputOverlayDrawableButton's button ID. | ||||||
|  |      */ | ||||||
|  |     public int getId() { | ||||||
|  |         return mButtonType; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getTrackId() { | ||||||
|  |         return mTrackId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setTrackId(int trackId) { | ||||||
|  |         mTrackId = trackId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean onConfigureTouch(MotionEvent event) { | ||||||
|  |         int pointerIndex = event.getActionIndex(); | ||||||
|  |         int fingerPositionX = (int) event.getX(pointerIndex); | ||||||
|  |         int fingerPositionY = (int) event.getY(pointerIndex); | ||||||
|  |         switch (event.getAction()) { | ||||||
|  |             case MotionEvent.ACTION_DOWN: | ||||||
|  |                 mPreviousTouchX = fingerPositionX; | ||||||
|  |                 mPreviousTouchY = fingerPositionY; | ||||||
|  |                 break; | ||||||
|  |             case MotionEvent.ACTION_MOVE: | ||||||
|  |                 mControlPositionX += fingerPositionX - mPreviousTouchX; | ||||||
|  |                 mControlPositionY += fingerPositionY - mPreviousTouchY; | ||||||
|  |                 setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, | ||||||
|  |                         getHeight() + mControlPositionY); | ||||||
|  |                 mPreviousTouchX = fingerPositionX; | ||||||
|  |                 mPreviousTouchY = fingerPositionY; | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setPosition(int x, int y) { | ||||||
|  |         mControlPositionX = x; | ||||||
|  |         mControlPositionY = y; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void draw(Canvas canvas) { | ||||||
|  |         getCurrentStateBitmapDrawable().draw(canvas); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private BitmapDrawable getCurrentStateBitmapDrawable() { | ||||||
|  |         return mPressedState ? mPressedStateBitmap : mDefaultStateBitmap; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setBounds(int left, int top, int right, int bottom) { | ||||||
|  |         mDefaultStateBitmap.setBounds(left, top, right, bottom); | ||||||
|  |         mPressedStateBitmap.setBounds(left, top, right, bottom); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Rect getBounds() { | ||||||
|  |         return mDefaultStateBitmap.getBounds(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getWidth() { | ||||||
|  |         return mWidth; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getHeight() { | ||||||
|  |         return mHeight; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setPressedState(boolean isPressed) { | ||||||
|  |         mPressedState = isPressed; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,193 @@ | ||||||
|  | /** | ||||||
|  |  * Copyright 2016 Dolphin Emulator Project | ||||||
|  |  * Licensed under GPLv2+ | ||||||
|  |  * Refer to the license.txt file included. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.overlay; | ||||||
|  | 
 | ||||||
|  | import android.content.res.Resources; | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.graphics.Rect; | ||||||
|  | import android.graphics.drawable.BitmapDrawable; | ||||||
|  | import android.view.MotionEvent; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Custom {@link BitmapDrawable} that is capable | ||||||
|  |  * of storing it's own ID. | ||||||
|  |  */ | ||||||
|  | public final class InputOverlayDrawableDpad { | ||||||
|  |     public static final int STATE_DEFAULT = 0; | ||||||
|  |     public static final int STATE_PRESSED_UP = 1; | ||||||
|  |     public static final int STATE_PRESSED_DOWN = 2; | ||||||
|  |     public static final int STATE_PRESSED_LEFT = 3; | ||||||
|  |     public static final int STATE_PRESSED_RIGHT = 4; | ||||||
|  |     public static final int STATE_PRESSED_UP_LEFT = 5; | ||||||
|  |     public static final int STATE_PRESSED_UP_RIGHT = 6; | ||||||
|  |     public static final int STATE_PRESSED_DOWN_LEFT = 7; | ||||||
|  |     public static final int STATE_PRESSED_DOWN_RIGHT = 8; | ||||||
|  |     public static final float VIRT_AXIS_DEADZONE = 0.5f; | ||||||
|  |     // The ID identifying what type of button this Drawable represents. | ||||||
|  |     private int[] mButtonType = new int[4]; | ||||||
|  |     private int mTrackId; | ||||||
|  |     private int mPreviousTouchX, mPreviousTouchY; | ||||||
|  |     private int mControlPositionX, mControlPositionY; | ||||||
|  |     private int mWidth; | ||||||
|  |     private int mHeight; | ||||||
|  |     private BitmapDrawable mDefaultStateBitmap; | ||||||
|  |     private BitmapDrawable mPressedOneDirectionStateBitmap; | ||||||
|  |     private BitmapDrawable mPressedTwoDirectionsStateBitmap; | ||||||
|  |     private int mPressState = STATE_DEFAULT; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constructor | ||||||
|  |      * | ||||||
|  |      * @param res                             {@link Resources} instance. | ||||||
|  |      * @param defaultStateBitmap              {@link Bitmap} of the default state. | ||||||
|  |      * @param pressedOneDirectionStateBitmap  {@link Bitmap} of the pressed state in one direction. | ||||||
|  |      * @param pressedTwoDirectionsStateBitmap {@link Bitmap} of the pressed state in two direction. | ||||||
|  |      * @param buttonUp                        Identifier for the up button. | ||||||
|  |      * @param buttonDown                      Identifier for the down button. | ||||||
|  |      * @param buttonLeft                      Identifier for the left button. | ||||||
|  |      * @param buttonRight                     Identifier for the right button. | ||||||
|  |      */ | ||||||
|  |     public InputOverlayDrawableDpad(Resources res, | ||||||
|  |                                     Bitmap defaultStateBitmap, | ||||||
|  |                                     Bitmap pressedOneDirectionStateBitmap, | ||||||
|  |                                     Bitmap pressedTwoDirectionsStateBitmap, | ||||||
|  |                                     int buttonUp, int buttonDown, | ||||||
|  |                                     int buttonLeft, int buttonRight) { | ||||||
|  |         mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); | ||||||
|  |         mPressedOneDirectionStateBitmap = new BitmapDrawable(res, pressedOneDirectionStateBitmap); | ||||||
|  |         mPressedTwoDirectionsStateBitmap = new BitmapDrawable(res, pressedTwoDirectionsStateBitmap); | ||||||
|  | 
 | ||||||
|  |         mWidth = mDefaultStateBitmap.getIntrinsicWidth(); | ||||||
|  |         mHeight = mDefaultStateBitmap.getIntrinsicHeight(); | ||||||
|  | 
 | ||||||
|  |         mButtonType[0] = buttonUp; | ||||||
|  |         mButtonType[1] = buttonDown; | ||||||
|  |         mButtonType[2] = buttonLeft; | ||||||
|  |         mButtonType[3] = buttonRight; | ||||||
|  | 
 | ||||||
|  |         mTrackId = -1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void draw(Canvas canvas) { | ||||||
|  |         int px = mControlPositionX + (getWidth() / 2); | ||||||
|  |         int py = mControlPositionY + (getHeight() / 2); | ||||||
|  |         switch (mPressState) { | ||||||
|  |             case STATE_DEFAULT: | ||||||
|  |                 mDefaultStateBitmap.draw(canvas); | ||||||
|  |                 break; | ||||||
|  |             case STATE_PRESSED_UP: | ||||||
|  |                 mPressedOneDirectionStateBitmap.draw(canvas); | ||||||
|  |                 break; | ||||||
|  |             case STATE_PRESSED_RIGHT: | ||||||
|  |                 canvas.save(); | ||||||
|  |                 canvas.rotate(90, px, py); | ||||||
|  |                 mPressedOneDirectionStateBitmap.draw(canvas); | ||||||
|  |                 canvas.restore(); | ||||||
|  |                 break; | ||||||
|  |             case STATE_PRESSED_DOWN: | ||||||
|  |                 canvas.save(); | ||||||
|  |                 canvas.rotate(180, px, py); | ||||||
|  |                 mPressedOneDirectionStateBitmap.draw(canvas); | ||||||
|  |                 canvas.restore(); | ||||||
|  |                 break; | ||||||
|  |             case STATE_PRESSED_LEFT: | ||||||
|  |                 canvas.save(); | ||||||
|  |                 canvas.rotate(270, px, py); | ||||||
|  |                 mPressedOneDirectionStateBitmap.draw(canvas); | ||||||
|  |                 canvas.restore(); | ||||||
|  |                 break; | ||||||
|  |             case STATE_PRESSED_UP_LEFT: | ||||||
|  |                 mPressedTwoDirectionsStateBitmap.draw(canvas); | ||||||
|  |                 break; | ||||||
|  |             case STATE_PRESSED_UP_RIGHT: | ||||||
|  |                 canvas.save(); | ||||||
|  |                 canvas.rotate(90, px, py); | ||||||
|  |                 mPressedTwoDirectionsStateBitmap.draw(canvas); | ||||||
|  |                 canvas.restore(); | ||||||
|  |                 break; | ||||||
|  |             case STATE_PRESSED_DOWN_RIGHT: | ||||||
|  |                 canvas.save(); | ||||||
|  |                 canvas.rotate(180, px, py); | ||||||
|  |                 mPressedTwoDirectionsStateBitmap.draw(canvas); | ||||||
|  |                 canvas.restore(); | ||||||
|  |                 break; | ||||||
|  |             case STATE_PRESSED_DOWN_LEFT: | ||||||
|  |                 canvas.save(); | ||||||
|  |                 canvas.rotate(270, px, py); | ||||||
|  |                 mPressedTwoDirectionsStateBitmap.draw(canvas); | ||||||
|  |                 canvas.restore(); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets one of the InputOverlayDrawableDpad's button IDs. | ||||||
|  |      * | ||||||
|  |      * @return the requested InputOverlayDrawableDpad's button ID. | ||||||
|  |      */ | ||||||
|  |     public int getId(int direction) { | ||||||
|  |         return mButtonType[direction]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getTrackId() { | ||||||
|  |         return mTrackId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setTrackId(int trackId) { | ||||||
|  |         mTrackId = trackId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean onConfigureTouch(MotionEvent event) { | ||||||
|  |         int pointerIndex = event.getActionIndex(); | ||||||
|  |         int fingerPositionX = (int) event.getX(pointerIndex); | ||||||
|  |         int fingerPositionY = (int) event.getY(pointerIndex); | ||||||
|  |         switch (event.getAction()) { | ||||||
|  |             case MotionEvent.ACTION_DOWN: | ||||||
|  |                 mPreviousTouchX = fingerPositionX; | ||||||
|  |                 mPreviousTouchY = fingerPositionY; | ||||||
|  |                 break; | ||||||
|  |             case MotionEvent.ACTION_MOVE: | ||||||
|  |                 mControlPositionX += fingerPositionX - mPreviousTouchX; | ||||||
|  |                 mControlPositionY += fingerPositionY - mPreviousTouchY; | ||||||
|  |                 setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, | ||||||
|  |                         getHeight() + mControlPositionY); | ||||||
|  |                 mPreviousTouchX = fingerPositionX; | ||||||
|  |                 mPreviousTouchY = fingerPositionY; | ||||||
|  |                 break; | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setPosition(int x, int y) { | ||||||
|  |         mControlPositionX = x; | ||||||
|  |         mControlPositionY = y; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setBounds(int left, int top, int right, int bottom) { | ||||||
|  |         mDefaultStateBitmap.setBounds(left, top, right, bottom); | ||||||
|  |         mPressedOneDirectionStateBitmap.setBounds(left, top, right, bottom); | ||||||
|  |         mPressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Rect getBounds() { | ||||||
|  |         return mDefaultStateBitmap.getBounds(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getWidth() { | ||||||
|  |         return mWidth; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getHeight() { | ||||||
|  |         return mHeight; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setState(int pressState) { | ||||||
|  |         mPressState = pressState; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,264 @@ | ||||||
|  | /** | ||||||
|  |  * Copyright 2013 Dolphin Emulator Project | ||||||
|  |  * Licensed under GPLv2+ | ||||||
|  |  * Refer to the license.txt file included. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.overlay; | ||||||
|  | 
 | ||||||
|  | import android.content.res.Resources; | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.graphics.Rect; | ||||||
|  | import android.graphics.drawable.BitmapDrawable; | ||||||
|  | import android.view.MotionEvent; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary.ButtonType; | ||||||
|  | import org.citra.citra_emu.utils.EmulationMenuSettings; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Custom {@link BitmapDrawable} that is capable | ||||||
|  |  * of storing it's own ID. | ||||||
|  |  */ | ||||||
|  | public final class InputOverlayDrawableJoystick { | ||||||
|  |     private final int[] axisIDs = {0, 0, 0, 0}; | ||||||
|  |     private final float[] axises = {0f, 0f}; | ||||||
|  |     private int trackId = -1; | ||||||
|  |     private int mJoystickType; | ||||||
|  |     private int mControlPositionX, mControlPositionY; | ||||||
|  |     private int mPreviousTouchX, mPreviousTouchY; | ||||||
|  |     private int mWidth; | ||||||
|  |     private int mHeight; | ||||||
|  |     private Rect mVirtBounds; | ||||||
|  |     private Rect mOrigBounds; | ||||||
|  |     private BitmapDrawable mOuterBitmap; | ||||||
|  |     private BitmapDrawable mDefaultStateInnerBitmap; | ||||||
|  |     private BitmapDrawable mPressedStateInnerBitmap; | ||||||
|  |     private BitmapDrawable mBoundsBoxBitmap; | ||||||
|  |     private boolean mPressedState = false; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constructor | ||||||
|  |      * | ||||||
|  |      * @param res                {@link Resources} instance. | ||||||
|  |      * @param bitmapOuter        {@link Bitmap} which represents the outer non-movable part of the joystick. | ||||||
|  |      * @param bitmapInnerDefault {@link Bitmap} which represents the default inner movable part of the joystick. | ||||||
|  |      * @param bitmapInnerPressed {@link Bitmap} which represents the pressed inner movable part of the joystick. | ||||||
|  |      * @param rectOuter          {@link Rect} which represents the outer joystick bounds. | ||||||
|  |      * @param rectInner          {@link Rect} which represents the inner joystick bounds. | ||||||
|  |      * @param joystick           Identifier for which joystick this is. | ||||||
|  |      */ | ||||||
|  |     public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter, | ||||||
|  |                                         Bitmap bitmapInnerDefault, Bitmap bitmapInnerPressed, | ||||||
|  |                                         Rect rectOuter, Rect rectInner, int joystick) { | ||||||
|  |         axisIDs[0] = joystick + 1; // Up | ||||||
|  |         axisIDs[1] = joystick + 2; // Down | ||||||
|  |         axisIDs[2] = joystick + 3; // Left | ||||||
|  |         axisIDs[3] = joystick + 4; // Right | ||||||
|  |         mJoystickType = joystick; | ||||||
|  | 
 | ||||||
|  |         mOuterBitmap = new BitmapDrawable(res, bitmapOuter); | ||||||
|  |         mDefaultStateInnerBitmap = new BitmapDrawable(res, bitmapInnerDefault); | ||||||
|  |         mPressedStateInnerBitmap = new BitmapDrawable(res, bitmapInnerPressed); | ||||||
|  |         mBoundsBoxBitmap = new BitmapDrawable(res, bitmapOuter); | ||||||
|  |         mWidth = bitmapOuter.getWidth(); | ||||||
|  |         mHeight = bitmapOuter.getHeight(); | ||||||
|  | 
 | ||||||
|  |         setBounds(rectOuter); | ||||||
|  |         mDefaultStateInnerBitmap.setBounds(rectInner); | ||||||
|  |         mPressedStateInnerBitmap.setBounds(rectInner); | ||||||
|  |         mVirtBounds = getBounds(); | ||||||
|  |         mOrigBounds = mOuterBitmap.copyBounds(); | ||||||
|  |         mBoundsBoxBitmap.setAlpha(0); | ||||||
|  |         mBoundsBoxBitmap.setBounds(getVirtBounds()); | ||||||
|  |         SetInnerBounds(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets this InputOverlayDrawableJoystick's button ID. | ||||||
|  |      * | ||||||
|  |      * @return this InputOverlayDrawableJoystick's button ID. | ||||||
|  |      */ | ||||||
|  |     public int getId() { | ||||||
|  |         return mJoystickType; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void draw(Canvas canvas) { | ||||||
|  |         mOuterBitmap.draw(canvas); | ||||||
|  |         getCurrentStateBitmapDrawable().draw(canvas); | ||||||
|  |         mBoundsBoxBitmap.draw(canvas); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void TrackEvent(MotionEvent event) { | ||||||
|  |         int pointerIndex = event.getActionIndex(); | ||||||
|  | 
 | ||||||
|  |         switch (event.getAction() & MotionEvent.ACTION_MASK) { | ||||||
|  |             case MotionEvent.ACTION_DOWN: | ||||||
|  |             case MotionEvent.ACTION_POINTER_DOWN: | ||||||
|  |                 if (getBounds().contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { | ||||||
|  |                     mPressedState = true; | ||||||
|  |                     mOuterBitmap.setAlpha(0); | ||||||
|  |                     mBoundsBoxBitmap.setAlpha(255); | ||||||
|  |                     if (EmulationMenuSettings.getJoystickRelCenter()) { | ||||||
|  |                         getVirtBounds().offset((int) event.getX(pointerIndex) - getVirtBounds().centerX(), | ||||||
|  |                                 (int) event.getY(pointerIndex) - getVirtBounds().centerY()); | ||||||
|  |                     } | ||||||
|  |                     mBoundsBoxBitmap.setBounds(getVirtBounds()); | ||||||
|  |                     trackId = event.getPointerId(pointerIndex); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case MotionEvent.ACTION_UP: | ||||||
|  |             case MotionEvent.ACTION_POINTER_UP: | ||||||
|  |                 if (trackId == event.getPointerId(pointerIndex)) { | ||||||
|  |                     mPressedState = false; | ||||||
|  |                     axises[0] = axises[1] = 0.0f; | ||||||
|  |                     mOuterBitmap.setAlpha(255); | ||||||
|  |                     mBoundsBoxBitmap.setAlpha(0); | ||||||
|  |                     setVirtBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, | ||||||
|  |                             mOrigBounds.bottom)); | ||||||
|  |                     setBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, | ||||||
|  |                             mOrigBounds.bottom)); | ||||||
|  |                     SetInnerBounds(); | ||||||
|  |                     trackId = -1; | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (trackId == -1) | ||||||
|  |             return; | ||||||
|  | 
 | ||||||
|  |         for (int i = 0; i < event.getPointerCount(); i++) { | ||||||
|  |             if (trackId == event.getPointerId(i)) { | ||||||
|  |                 float touchX = event.getX(i); | ||||||
|  |                 float touchY = event.getY(i); | ||||||
|  |                 float maxY = getVirtBounds().bottom; | ||||||
|  |                 float maxX = getVirtBounds().right; | ||||||
|  |                 touchX -= getVirtBounds().centerX(); | ||||||
|  |                 maxX -= getVirtBounds().centerX(); | ||||||
|  |                 touchY -= getVirtBounds().centerY(); | ||||||
|  |                 maxY -= getVirtBounds().centerY(); | ||||||
|  |                 final float AxisX = touchX / maxX; | ||||||
|  |                 final float AxisY = touchY / maxY; | ||||||
|  | 
 | ||||||
|  |                 // Clamp the circle pad input to a circle | ||||||
|  |                 final float angle = (float) Math.atan2(AxisY, AxisX); | ||||||
|  |                 float radius = (float) Math.sqrt(AxisX * AxisX + AxisY * AxisY); | ||||||
|  |                 if(radius > 1.0f) | ||||||
|  |                 { | ||||||
|  |                     radius = 1.0f; | ||||||
|  |                 } | ||||||
|  |                 axises[0] = ((float)Math.cos(angle) * radius); | ||||||
|  |                 axises[1] = ((float)Math.sin(angle) * radius); | ||||||
|  |                 SetInnerBounds(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean onConfigureTouch(MotionEvent event) { | ||||||
|  |         int pointerIndex = event.getActionIndex(); | ||||||
|  |         int fingerPositionX = (int) event.getX(pointerIndex); | ||||||
|  |         int fingerPositionY = (int) event.getY(pointerIndex); | ||||||
|  | 
 | ||||||
|  |         int scale = 1; | ||||||
|  |         if (mJoystickType == ButtonType.STICK_C) { | ||||||
|  |             // C-stick is scaled down to be half the size of the circle pad | ||||||
|  |             scale = 2; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         switch (event.getAction()) { | ||||||
|  |             case MotionEvent.ACTION_DOWN: | ||||||
|  |                 mPreviousTouchX = fingerPositionX; | ||||||
|  |                 mPreviousTouchY = fingerPositionY; | ||||||
|  |                 break; | ||||||
|  |             case MotionEvent.ACTION_MOVE: | ||||||
|  |                 int deltaX = fingerPositionX - mPreviousTouchX; | ||||||
|  |                 int deltaY = fingerPositionY - mPreviousTouchY; | ||||||
|  |                 mControlPositionX += deltaX; | ||||||
|  |                 mControlPositionY += deltaY; | ||||||
|  |                 setBounds(new Rect(mControlPositionX, mControlPositionY, | ||||||
|  |                         mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, | ||||||
|  |                         mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)); | ||||||
|  |                 setVirtBounds(new Rect(mControlPositionX, mControlPositionY, | ||||||
|  |                         mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, | ||||||
|  |                         mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)); | ||||||
|  |                 SetInnerBounds(); | ||||||
|  |                 setOrigBounds(new Rect(new Rect(mControlPositionX, mControlPositionY, | ||||||
|  |                         mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, | ||||||
|  |                         mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY))); | ||||||
|  |                 mPreviousTouchX = fingerPositionX; | ||||||
|  |                 mPreviousTouchY = fingerPositionY; | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     public float[] getAxisValues() { | ||||||
|  |         return axises; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int[] getAxisIDs() { | ||||||
|  |         return axisIDs; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void SetInnerBounds() { | ||||||
|  |         int X = getVirtBounds().centerX() + (int) ((axises[0]) * (getVirtBounds().width() / 2)); | ||||||
|  |         int Y = getVirtBounds().centerY() + (int) ((axises[1]) * (getVirtBounds().height() / 2)); | ||||||
|  | 
 | ||||||
|  |         if (mJoystickType == ButtonType.STICK_LEFT) { | ||||||
|  |             X += 1; | ||||||
|  |             Y += 1; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (X > getVirtBounds().centerX() + (getVirtBounds().width() / 2)) | ||||||
|  |             X = getVirtBounds().centerX() + (getVirtBounds().width() / 2); | ||||||
|  |         if (X < getVirtBounds().centerX() - (getVirtBounds().width() / 2)) | ||||||
|  |             X = getVirtBounds().centerX() - (getVirtBounds().width() / 2); | ||||||
|  |         if (Y > getVirtBounds().centerY() + (getVirtBounds().height() / 2)) | ||||||
|  |             Y = getVirtBounds().centerY() + (getVirtBounds().height() / 2); | ||||||
|  |         if (Y < getVirtBounds().centerY() - (getVirtBounds().height() / 2)) | ||||||
|  |             Y = getVirtBounds().centerY() - (getVirtBounds().height() / 2); | ||||||
|  | 
 | ||||||
|  |         int width = mPressedStateInnerBitmap.getBounds().width() / 2; | ||||||
|  |         int height = mPressedStateInnerBitmap.getBounds().height() / 2; | ||||||
|  |         mDefaultStateInnerBitmap.setBounds(X - width, Y - height, X + width, Y + height); | ||||||
|  |         mPressedStateInnerBitmap.setBounds(mDefaultStateInnerBitmap.getBounds()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setPosition(int x, int y) { | ||||||
|  |         mControlPositionX = x; | ||||||
|  |         mControlPositionY = y; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private BitmapDrawable getCurrentStateBitmapDrawable() { | ||||||
|  |         return mPressedState ? mPressedStateInnerBitmap : mDefaultStateInnerBitmap; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Rect getBounds() { | ||||||
|  |         return mOuterBitmap.getBounds(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setBounds(Rect bounds) { | ||||||
|  |         mOuterBitmap.setBounds(bounds); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setOrigBounds(Rect bounds) { | ||||||
|  |         mOrigBounds = bounds; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private Rect getVirtBounds() { | ||||||
|  |         return mVirtBounds; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setVirtBounds(Rect bounds) { | ||||||
|  |         mVirtBounds = bounds; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getWidth() { | ||||||
|  |         return mWidth; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getHeight() { | ||||||
|  |         return mHeight; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,130 @@ | ||||||
|  | package org.citra.citra_emu.ui; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.res.TypedArray; | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.graphics.Rect; | ||||||
|  | import android.graphics.drawable.Drawable; | ||||||
|  | import android.util.AttributeSet; | ||||||
|  | import android.view.View; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Implementation from: | ||||||
|  |  * https://gist.github.com/lapastillaroja/858caf1a82791b6c1a36 | ||||||
|  |  */ | ||||||
|  | public class DividerItemDecoration extends RecyclerView.ItemDecoration { | ||||||
|  | 
 | ||||||
|  |     private Drawable mDivider; | ||||||
|  |     private boolean mShowFirstDivider = false; | ||||||
|  |     private boolean mShowLastDivider = false; | ||||||
|  | 
 | ||||||
|  |     public DividerItemDecoration(Context context, AttributeSet attrs) { | ||||||
|  |         final TypedArray a = context | ||||||
|  |                 .obtainStyledAttributes(attrs, new int[]{android.R.attr.listDivider}); | ||||||
|  |         mDivider = a.getDrawable(0); | ||||||
|  |         a.recycle(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public DividerItemDecoration(Context context, AttributeSet attrs, boolean showFirstDivider, | ||||||
|  |                                  boolean showLastDivider) { | ||||||
|  |         this(context, attrs); | ||||||
|  |         mShowFirstDivider = showFirstDivider; | ||||||
|  |         mShowLastDivider = showLastDivider; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public DividerItemDecoration(Drawable divider) { | ||||||
|  |         mDivider = divider; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public DividerItemDecoration(Drawable divider, boolean showFirstDivider, | ||||||
|  |                                  boolean showLastDivider) { | ||||||
|  |         this(divider); | ||||||
|  |         mShowFirstDivider = showFirstDivider; | ||||||
|  |         mShowLastDivider = showLastDivider; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, | ||||||
|  |                                @NonNull RecyclerView.State state) { | ||||||
|  |         super.getItemOffsets(outRect, view, parent, state); | ||||||
|  |         if (mDivider == null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         if (parent.getChildAdapterPosition(view) < 1) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (getOrientation(parent) == LinearLayoutManager.VERTICAL) { | ||||||
|  |             outRect.top = mDivider.getIntrinsicHeight(); | ||||||
|  |         } else { | ||||||
|  |             outRect.left = mDivider.getIntrinsicWidth(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { | ||||||
|  |         if (mDivider == null) { | ||||||
|  |             super.onDrawOver(c, parent, state); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Initialization needed to avoid compiler warning | ||||||
|  |         int left = 0, right = 0, top = 0, bottom = 0, size; | ||||||
|  |         int orientation = getOrientation(parent); | ||||||
|  |         int childCount = parent.getChildCount(); | ||||||
|  | 
 | ||||||
|  |         if (orientation == LinearLayoutManager.VERTICAL) { | ||||||
|  |             size = mDivider.getIntrinsicHeight(); | ||||||
|  |             left = parent.getPaddingLeft(); | ||||||
|  |             right = parent.getWidth() - parent.getPaddingRight(); | ||||||
|  |         } else { //horizontal | ||||||
|  |             size = mDivider.getIntrinsicWidth(); | ||||||
|  |             top = parent.getPaddingTop(); | ||||||
|  |             bottom = parent.getHeight() - parent.getPaddingBottom(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (int i = mShowFirstDivider ? 0 : 1; i < childCount; i++) { | ||||||
|  |             View child = parent.getChildAt(i); | ||||||
|  |             RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); | ||||||
|  | 
 | ||||||
|  |             if (orientation == LinearLayoutManager.VERTICAL) { | ||||||
|  |                 top = child.getTop() - params.topMargin; | ||||||
|  |                 bottom = top + size; | ||||||
|  |             } else { //horizontal | ||||||
|  |                 left = child.getLeft() - params.leftMargin; | ||||||
|  |                 right = left + size; | ||||||
|  |             } | ||||||
|  |             mDivider.setBounds(left, top, right, bottom); | ||||||
|  |             mDivider.draw(c); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // show last divider | ||||||
|  |         if (mShowLastDivider && childCount > 0) { | ||||||
|  |             View child = parent.getChildAt(childCount - 1); | ||||||
|  |             RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); | ||||||
|  |             if (orientation == LinearLayoutManager.VERTICAL) { | ||||||
|  |                 top = child.getBottom() + params.bottomMargin; | ||||||
|  |                 bottom = top + size; | ||||||
|  |             } else { // horizontal | ||||||
|  |                 left = child.getRight() + params.rightMargin; | ||||||
|  |                 right = left + size; | ||||||
|  |             } | ||||||
|  |             mDivider.setBounds(left, top, right, bottom); | ||||||
|  |             mDivider.draw(c); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private int getOrientation(RecyclerView parent) { | ||||||
|  |         if (parent.getLayoutManager() instanceof LinearLayoutManager) { | ||||||
|  |             LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager(); | ||||||
|  |             return layoutManager.getOrientation(); | ||||||
|  |         } else { | ||||||
|  |             throw new IllegalStateException( | ||||||
|  |                     "DividerItemDecoration can only be used with a LinearLayoutManager."); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,269 @@ | ||||||
|  | package org.citra.citra_emu.ui.main; | ||||||
|  | 
 | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.content.pm.PackageManager; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.view.Menu; | ||||||
|  | import android.view.MenuInflater; | ||||||
|  | import android.view.MenuItem; | ||||||
|  | import android.widget.Toast; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.appcompat.app.AppCompatActivity; | ||||||
|  | import androidx.appcompat.widget.Toolbar; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.activities.EmulationActivity; | ||||||
|  | import org.citra.citra_emu.features.settings.ui.SettingsActivity; | ||||||
|  | import org.citra.citra_emu.model.GameProvider; | ||||||
|  | import org.citra.citra_emu.ui.platform.PlatformGamesFragment; | ||||||
|  | import org.citra.citra_emu.utils.AddDirectoryHelper; | ||||||
|  | import org.citra.citra_emu.utils.BillingManager; | ||||||
|  | import org.citra.citra_emu.utils.DirectoryInitialization; | ||||||
|  | import org.citra.citra_emu.utils.FileBrowserHelper; | ||||||
|  | import org.citra.citra_emu.utils.PermissionsHandler; | ||||||
|  | import org.citra.citra_emu.utils.PicassoUtils; | ||||||
|  | import org.citra.citra_emu.utils.StartupHandler; | ||||||
|  | import org.citra.citra_emu.utils.ThemeUtil; | ||||||
|  | 
 | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which | ||||||
|  |  * individually display a grid of available games for each Fragment, in a tabbed layout. | ||||||
|  |  */ | ||||||
|  | public final class MainActivity extends AppCompatActivity implements MainView { | ||||||
|  |     private Toolbar mToolbar; | ||||||
|  |     private int mFrameLayoutId; | ||||||
|  |     private PlatformGamesFragment mPlatformGamesFragment; | ||||||
|  | 
 | ||||||
|  |     private MainPresenter mPresenter = new MainPresenter(this); | ||||||
|  | 
 | ||||||
|  |     // Singleton to manage user billing state | ||||||
|  |     private static BillingManager mBillingManager; | ||||||
|  | 
 | ||||||
|  |     private static MenuItem mPremiumButton; | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onCreate(Bundle savedInstanceState) { | ||||||
|  |         ThemeUtil.applyTheme(); | ||||||
|  | 
 | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |         setContentView(R.layout.activity_main); | ||||||
|  | 
 | ||||||
|  |         findViews(); | ||||||
|  | 
 | ||||||
|  |         setSupportActionBar(mToolbar); | ||||||
|  | 
 | ||||||
|  |         mFrameLayoutId = R.id.games_platform_frame; | ||||||
|  |         mPresenter.onCreate(); | ||||||
|  | 
 | ||||||
|  |         if (savedInstanceState == null) { | ||||||
|  |             StartupHandler.HandleInit(this); | ||||||
|  |             if (PermissionsHandler.hasWriteAccess(this)) { | ||||||
|  |                 mPlatformGamesFragment = new PlatformGamesFragment(); | ||||||
|  |                 getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment) | ||||||
|  |                         .commit(); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment"); | ||||||
|  |         } | ||||||
|  |         PicassoUtils.init(); | ||||||
|  | 
 | ||||||
|  |         // Setup billing manager, so we can globally query for Premium status | ||||||
|  |         mBillingManager = new BillingManager(this); | ||||||
|  | 
 | ||||||
|  |         // Dismiss previous notifications (should not happen unless a crash occurred) | ||||||
|  |         EmulationActivity.tryDismissRunningNotification(this); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onSaveInstanceState(@NonNull Bundle outState) { | ||||||
|  |         super.onSaveInstanceState(outState); | ||||||
|  |         if (PermissionsHandler.hasWriteAccess(this)) { | ||||||
|  |             if (getSupportFragmentManager() == null) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             if (outState == null) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onResume() { | ||||||
|  |         super.onResume(); | ||||||
|  |         mPresenter.addDirIfNeeded(new AddDirectoryHelper(this)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // TODO: Replace with a ButterKnife injection. | ||||||
|  |     private void findViews() { | ||||||
|  |         mToolbar = findViewById(R.id.toolbar_main); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean onCreateOptionsMenu(Menu menu) { | ||||||
|  |         MenuInflater inflater = getMenuInflater(); | ||||||
|  |         inflater.inflate(R.menu.menu_game_grid, menu); | ||||||
|  |         mPremiumButton = menu.findItem(R.id.button_premium); | ||||||
|  | 
 | ||||||
|  |         if (mBillingManager.isPremiumCached()) { | ||||||
|  |             // User had premium in a previous session, hide upsell option | ||||||
|  |             setPremiumButtonVisible(false); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static public void setPremiumButtonVisible(boolean isVisible) { | ||||||
|  |         if (mPremiumButton != null) { | ||||||
|  |             mPremiumButton.setVisible(isVisible); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * MainView | ||||||
|  |      */ | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void setVersionString(String version) { | ||||||
|  |         mToolbar.setSubtitle(version); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void refresh() { | ||||||
|  |         getContentResolver().insert(GameProvider.URI_REFRESH, null); | ||||||
|  |         refreshFragment(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void launchSettingsActivity(String menuTag) { | ||||||
|  |         if (PermissionsHandler.hasWriteAccess(this)) { | ||||||
|  |             SettingsActivity.launch(this, menuTag, ""); | ||||||
|  |         } else { | ||||||
|  |             PermissionsHandler.checkWritePermission(this); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void launchFileListActivity(int request) { | ||||||
|  |         if (PermissionsHandler.hasWriteAccess(this)) { | ||||||
|  |             switch (request) { | ||||||
|  |                 case MainPresenter.REQUEST_ADD_DIRECTORY: | ||||||
|  |                     FileBrowserHelper.openDirectoryPicker(this, | ||||||
|  |                                                       MainPresenter.REQUEST_ADD_DIRECTORY, | ||||||
|  |                                                       R.string.select_game_folder, | ||||||
|  |                                                       Arrays.asList("elf", "axf", "cci", "3ds", | ||||||
|  |                                                                     "cxi", "app", "3dsx", "cia", | ||||||
|  |                                                                     "rar", "zip", "7z", "torrent", | ||||||
|  |                                                                     "tar", "gz")); | ||||||
|  |                     break; | ||||||
|  |                 case MainPresenter.REQUEST_INSTALL_CIA: | ||||||
|  |                     FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_INSTALL_CIA, | ||||||
|  |                                                      R.string.install_cia_title, | ||||||
|  |                                                      Collections.singletonList("cia"), true); | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             PermissionsHandler.checkWritePermission(this); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @param requestCode An int describing whether the Activity that is returning did so successfully. | ||||||
|  |      * @param resultCode  An int describing what Activity is giving us this callback. | ||||||
|  |      * @param result      The information the returning Activity is providing us. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     protected void onActivityResult(int requestCode, int resultCode, Intent result) { | ||||||
|  |         super.onActivityResult(requestCode, resultCode, result); | ||||||
|  |         switch (requestCode) { | ||||||
|  |             case MainPresenter.REQUEST_ADD_DIRECTORY: | ||||||
|  |                 // If the user picked a file, as opposed to just backing out. | ||||||
|  |                 if (resultCode == MainActivity.RESULT_OK) { | ||||||
|  |                     // When a new directory is picked, we currently will reset the existing games | ||||||
|  |                     // database. This effectively means that only one game directory is supported. | ||||||
|  |                     // TODO(bunnei): Consider fixing this in the future, or removing code for this. | ||||||
|  |                     getContentResolver().insert(GameProvider.URI_RESET, null); | ||||||
|  |                     // Add the new directory | ||||||
|  |                     mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedDirectory(result)); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |                 case MainPresenter.REQUEST_INSTALL_CIA: | ||||||
|  |                     // If the user picked a file, as opposed to just backing out. | ||||||
|  |                     if (resultCode == MainActivity.RESULT_OK) { | ||||||
|  |                         NativeLibrary.InstallCIAS(FileBrowserHelper.getSelectedFiles(result)); | ||||||
|  |                         mPresenter.refeshGameList(); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { | ||||||
|  |         switch (requestCode) { | ||||||
|  |             case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION: | ||||||
|  |                 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { | ||||||
|  |                     DirectoryInitialization.start(this); | ||||||
|  | 
 | ||||||
|  |                     mPlatformGamesFragment = new PlatformGamesFragment(); | ||||||
|  |                     getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment) | ||||||
|  |                             .commit(); | ||||||
|  | 
 | ||||||
|  |                     // Immediately prompt user to select a game directory on first boot | ||||||
|  |                     if (mPresenter != null) { | ||||||
|  |                         mPresenter.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT) | ||||||
|  |                             .show(); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 super.onRequestPermissionsResult(requestCode, permissions, grantResults); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Called by the framework whenever any actionbar/toolbar icon is clicked. | ||||||
|  |      * | ||||||
|  |      * @param item The icon that was clicked on. | ||||||
|  |      * @return True if the event was handled, false to bubble it up to the OS. | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public boolean onOptionsItemSelected(MenuItem item) { | ||||||
|  |         return mPresenter.handleOptionSelection(item.getItemId()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void refreshFragment() { | ||||||
|  |         if (mPlatformGamesFragment != null) { | ||||||
|  |             mPlatformGamesFragment.refresh(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void onDestroy() { | ||||||
|  |         EmulationActivity.tryDismissRunningNotification(this); | ||||||
|  |         super.onDestroy(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return true if Premium subscription is currently active | ||||||
|  |      */ | ||||||
|  |     public static boolean isPremiumActive() { | ||||||
|  |         return mBillingManager.isPremiumActive(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Invokes the billing flow for Premium | ||||||
|  |      * | ||||||
|  |      * @param callback Optional callback, called once, on completion of billing | ||||||
|  |      */ | ||||||
|  |     public static void invokePremiumBilling(Runnable callback) { | ||||||
|  |         mBillingManager.invokePremiumBilling(callback); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,82 @@ | ||||||
|  | package org.citra.citra_emu.ui.main; | ||||||
|  | 
 | ||||||
|  | import android.os.SystemClock; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.BuildConfig; | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.model.Settings; | ||||||
|  | import org.citra.citra_emu.features.settings.utils.SettingsFile; | ||||||
|  | import org.citra.citra_emu.model.GameDatabase; | ||||||
|  | import org.citra.citra_emu.utils.AddDirectoryHelper; | ||||||
|  | 
 | ||||||
|  | public final class MainPresenter { | ||||||
|  |     public static final int REQUEST_ADD_DIRECTORY = 1; | ||||||
|  |     public static final int REQUEST_INSTALL_CIA = 2; | ||||||
|  | 
 | ||||||
|  |     private final MainView mView; | ||||||
|  |     private String mDirToAdd; | ||||||
|  |     private long mLastClickTime = 0; | ||||||
|  | 
 | ||||||
|  |     public MainPresenter(MainView view) { | ||||||
|  |         mView = view; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onCreate() { | ||||||
|  |         String versionName = BuildConfig.VERSION_NAME; | ||||||
|  |         mView.setVersionString(versionName); | ||||||
|  |         refeshGameList(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void launchFileListActivity(int request) { | ||||||
|  |         if (mView != null) { | ||||||
|  |             mView.launchFileListActivity(request); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean handleOptionSelection(int itemId) { | ||||||
|  |         // Double-click prevention, using threshold of 500 ms | ||||||
|  |         if (SystemClock.elapsedRealtime() - mLastClickTime < 500) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         mLastClickTime = SystemClock.elapsedRealtime(); | ||||||
|  | 
 | ||||||
|  |         switch (itemId) { | ||||||
|  |             case R.id.menu_settings_core: | ||||||
|  |                 mView.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG); | ||||||
|  |                 return true; | ||||||
|  | 
 | ||||||
|  |             case R.id.button_add_directory: | ||||||
|  |                 launchFileListActivity(REQUEST_ADD_DIRECTORY); | ||||||
|  |                 return true; | ||||||
|  | 
 | ||||||
|  |             case R.id.button_install_cia: | ||||||
|  |                 launchFileListActivity(REQUEST_INSTALL_CIA); | ||||||
|  |                 return true; | ||||||
|  | 
 | ||||||
|  |             case R.id.button_premium: | ||||||
|  |                 mView.launchSettingsActivity(Settings.SECTION_PREMIUM); | ||||||
|  |                 return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void addDirIfNeeded(AddDirectoryHelper helper) { | ||||||
|  |         if (mDirToAdd != null) { | ||||||
|  |             helper.addDirectory(mDirToAdd, mView::refresh); | ||||||
|  | 
 | ||||||
|  |             mDirToAdd = null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onDirectorySelected(String dir) { | ||||||
|  |         mDirToAdd = dir; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void refeshGameList() { | ||||||
|  |         GameDatabase databaseHelper = CitraApplication.databaseHelper; | ||||||
|  |         databaseHelper.scanLibrary(databaseHelper.getWritableDatabase()); | ||||||
|  |         mView.refresh(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,25 @@ | ||||||
|  | package org.citra.citra_emu.ui.main; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Abstraction for the screen that shows on application launch. | ||||||
|  |  * Implementations will differ primarily to target touch-screen | ||||||
|  |  * or non-touch screen devices. | ||||||
|  |  */ | ||||||
|  | public interface MainView { | ||||||
|  |     /** | ||||||
|  |      * Pass the view the native library's version string. Displaying | ||||||
|  |      * it is optional. | ||||||
|  |      * | ||||||
|  |      * @param version A string pulled from native code. | ||||||
|  |      */ | ||||||
|  |     void setVersionString(String version); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Tell the view to refresh its contents. | ||||||
|  |      */ | ||||||
|  |     void refresh(); | ||||||
|  | 
 | ||||||
|  |     void launchSettingsActivity(String menuTag); | ||||||
|  | 
 | ||||||
|  |     void launchFileListActivity(int request); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,86 @@ | ||||||
|  | package org.citra.citra_emu.ui.platform; | ||||||
|  | 
 | ||||||
|  | import android.database.Cursor; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.core.content.ContextCompat; | ||||||
|  | import androidx.fragment.app.Fragment; | ||||||
|  | import androidx.recyclerview.widget.GridLayoutManager; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.adapters.GameAdapter; | ||||||
|  | import org.citra.citra_emu.model.GameDatabase; | ||||||
|  | 
 | ||||||
|  | public final class PlatformGamesFragment extends Fragment implements PlatformGamesView { | ||||||
|  |     private PlatformGamesPresenter mPresenter = new PlatformGamesPresenter(this); | ||||||
|  | 
 | ||||||
|  |     private GameAdapter mAdapter; | ||||||
|  |     private RecyclerView mRecyclerView; | ||||||
|  |     private TextView mTextView; | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||||
|  |         View rootView = inflater.inflate(R.layout.fragment_grid, container, false); | ||||||
|  | 
 | ||||||
|  |         findViews(rootView); | ||||||
|  | 
 | ||||||
|  |         mPresenter.onCreateView(); | ||||||
|  | 
 | ||||||
|  |         return rootView; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onViewCreated(View view, Bundle savedInstanceState) { | ||||||
|  |         int columns = getResources().getInteger(R.integer.game_grid_columns); | ||||||
|  |         RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), columns); | ||||||
|  |         mAdapter = new GameAdapter(); | ||||||
|  | 
 | ||||||
|  |         mRecyclerView.setLayoutManager(layoutManager); | ||||||
|  |         mRecyclerView.setAdapter(mAdapter); | ||||||
|  |         mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(ContextCompat.getDrawable(getActivity(), R.drawable.gamelist_divider), 1)); | ||||||
|  | 
 | ||||||
|  |         // Add swipe down to refresh gesture | ||||||
|  |         final SwipeRefreshLayout pullToRefresh = view.findViewById(R.id.refresh_grid_games); | ||||||
|  |         pullToRefresh.setOnRefreshListener(() -> { | ||||||
|  |             GameDatabase databaseHelper = CitraApplication.databaseHelper; | ||||||
|  |             databaseHelper.scanLibrary(databaseHelper.getWritableDatabase()); | ||||||
|  |             refresh(); | ||||||
|  |             pullToRefresh.setRefreshing(false); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void refresh() { | ||||||
|  |         mPresenter.refresh(); | ||||||
|  |         updateTextView(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void showGames(Cursor games) { | ||||||
|  |         if (mAdapter != null) { | ||||||
|  |             mAdapter.swapCursor(games); | ||||||
|  |         } | ||||||
|  |         updateTextView(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void updateTextView() { | ||||||
|  |         mTextView.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void findViews(View root) { | ||||||
|  |         mRecyclerView = root.findViewById(R.id.grid_games); | ||||||
|  |         mTextView = root.findViewById(R.id.gamelist_empty_text); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,42 @@ | ||||||
|  | package org.citra.citra_emu.ui.platform; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.model.GameDatabase; | ||||||
|  | import org.citra.citra_emu.utils.Log; | ||||||
|  | 
 | ||||||
|  | import rx.android.schedulers.AndroidSchedulers; | ||||||
|  | import rx.schedulers.Schedulers; | ||||||
|  | 
 | ||||||
|  | public final class PlatformGamesPresenter { | ||||||
|  |     private final PlatformGamesView mView; | ||||||
|  | 
 | ||||||
|  |     public PlatformGamesPresenter(PlatformGamesView view) { | ||||||
|  |         mView = view; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void onCreateView() { | ||||||
|  |         loadGames(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void refresh() { | ||||||
|  |         Log.debug("[PlatformGamesPresenter] : Refreshing..."); | ||||||
|  |         loadGames(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void loadGames() { | ||||||
|  |         Log.debug("[PlatformGamesPresenter] : Loading games..."); | ||||||
|  | 
 | ||||||
|  |         GameDatabase databaseHelper = CitraApplication.databaseHelper; | ||||||
|  | 
 | ||||||
|  |         databaseHelper.getGames() | ||||||
|  |                 .subscribeOn(Schedulers.io()) | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe(games -> | ||||||
|  |                 { | ||||||
|  |                     Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor..."); | ||||||
|  | 
 | ||||||
|  |                     mView.showGames(games); | ||||||
|  |                 }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,21 @@ | ||||||
|  | package org.citra.citra_emu.ui.platform; | ||||||
|  | 
 | ||||||
|  | import android.database.Cursor; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Abstraction for a screen representing a single platform's games. | ||||||
|  |  */ | ||||||
|  | public interface PlatformGamesView { | ||||||
|  |     /** | ||||||
|  |      * Tell the view to refresh its contents. | ||||||
|  |      */ | ||||||
|  |     void refresh(); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * To be called when an asynchronous database read completes. Passes the | ||||||
|  |      * result, in this case a {@link Cursor}, to the view. | ||||||
|  |      * | ||||||
|  |      * @param games A Cursor containing the games read from the database. | ||||||
|  |      */ | ||||||
|  |     void showGames(Cursor games); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | public interface Action1<T> { | ||||||
|  |     void call(T t); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.content.AsyncQueryHandler; | ||||||
|  | import android.content.ContentValues; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.net.Uri; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.model.GameDatabase; | ||||||
|  | import org.citra.citra_emu.model.GameProvider; | ||||||
|  | 
 | ||||||
|  | public class AddDirectoryHelper { | ||||||
|  |     private Context mContext; | ||||||
|  | 
 | ||||||
|  |     public AddDirectoryHelper(Context context) { | ||||||
|  |         this.mContext = context; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void addDirectory(String dir, AddDirectoryListener addDirectoryListener) { | ||||||
|  |         AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) { | ||||||
|  |             @Override | ||||||
|  |             protected void onInsertComplete(int token, Object cookie, Uri uri) { | ||||||
|  |                 addDirectoryListener.onDirectoryAdded(); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         ContentValues file = new ContentValues(); | ||||||
|  |         file.put(GameDatabase.KEY_FOLDER_PATH, dir); | ||||||
|  | 
 | ||||||
|  |         handler.startInsert(0,                // We don't need to identify this call to the handler | ||||||
|  |                 null,                        // We don't need to pass additional data to the handler | ||||||
|  |                 GameProvider.URI_FOLDER,    // Tell the GameProvider we are adding a folder | ||||||
|  |                 file); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public interface AddDirectoryListener { | ||||||
|  |         void onDirectoryAdded(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.Map; | ||||||
|  | 
 | ||||||
|  | public class BiMap<K, V> { | ||||||
|  |     private Map<K, V> forward = new HashMap<K, V>(); | ||||||
|  |     private Map<V, K> backward = new HashMap<V, K>(); | ||||||
|  | 
 | ||||||
|  |     public synchronized void add(K key, V value) { | ||||||
|  |         forward.put(key, value); | ||||||
|  |         backward.put(value, key); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public synchronized V getForward(K key) { | ||||||
|  |         return forward.get(key); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public synchronized K getBackward(V key) { | ||||||
|  |         return backward.get(key); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,215 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.app.Activity; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | import android.widget.Toast; | ||||||
|  | 
 | ||||||
|  | import com.android.billingclient.api.AcknowledgePurchaseParams; | ||||||
|  | import com.android.billingclient.api.AcknowledgePurchaseResponseListener; | ||||||
|  | import com.android.billingclient.api.BillingClient; | ||||||
|  | import com.android.billingclient.api.BillingClientStateListener; | ||||||
|  | import com.android.billingclient.api.BillingFlowParams; | ||||||
|  | import com.android.billingclient.api.BillingResult; | ||||||
|  | import com.android.billingclient.api.Purchase; | ||||||
|  | import com.android.billingclient.api.Purchase.PurchasesResult; | ||||||
|  | import com.android.billingclient.api.PurchasesUpdatedListener; | ||||||
|  | import com.android.billingclient.api.SkuDetails; | ||||||
|  | import com.android.billingclient.api.SkuDetailsParams; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.features.settings.utils.SettingsFile; | ||||||
|  | import org.citra.citra_emu.ui.main.MainActivity; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | public class BillingManager implements PurchasesUpdatedListener { | ||||||
|  |     private final String BILLING_SKU_PREMIUM = "citra.citra_emu.product_id.premium"; | ||||||
|  | 
 | ||||||
|  |     private final Activity mActivity; | ||||||
|  |     private BillingClient mBillingClient; | ||||||
|  |     private SkuDetails mSkuPremium; | ||||||
|  |     private boolean mIsPremiumActive = false; | ||||||
|  |     private boolean mIsServiceConnected = false; | ||||||
|  |     private Runnable mUpdateBillingCallback; | ||||||
|  | 
 | ||||||
|  |     private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); | ||||||
|  | 
 | ||||||
|  |     public BillingManager(Activity activity) { | ||||||
|  |         mActivity = activity; | ||||||
|  |         mBillingClient = BillingClient.newBuilder(mActivity).enablePendingPurchases().setListener(this).build(); | ||||||
|  |         querySkuDetails(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static public boolean isPremiumCached() { | ||||||
|  |         return mPreferences.getBoolean(SettingsFile.KEY_PREMIUM, false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return true if Premium subscription is currently active | ||||||
|  |      */ | ||||||
|  |     public boolean isPremiumActive() { | ||||||
|  |         return mIsPremiumActive; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Invokes the billing flow for Premium | ||||||
|  |      * | ||||||
|  |      * @param callback Optional callback, called once, on completion of billing | ||||||
|  |      */ | ||||||
|  |     public void invokePremiumBilling(Runnable callback) { | ||||||
|  |         if (mSkuPremium == null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Optional callback to refresh the UI for the caller when billing completes | ||||||
|  |         mUpdateBillingCallback = callback; | ||||||
|  | 
 | ||||||
|  |         // Invoke the billing flow | ||||||
|  |         BillingFlowParams flowParams = BillingFlowParams.newBuilder() | ||||||
|  |                 .setSkuDetails(mSkuPremium) | ||||||
|  |                 .build(); | ||||||
|  |         mBillingClient.launchBillingFlow(mActivity, flowParams); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void updatePremiumState(boolean isPremiumActive) { | ||||||
|  |         mIsPremiumActive = isPremiumActive; | ||||||
|  | 
 | ||||||
|  |         // Cache state for synchronous UI | ||||||
|  |         SharedPreferences.Editor editor = mPreferences.edit(); | ||||||
|  |         editor.putBoolean(SettingsFile.KEY_PREMIUM, isPremiumActive); | ||||||
|  |         editor.apply(); | ||||||
|  | 
 | ||||||
|  |         // No need to show button in action bar if Premium is active | ||||||
|  |         MainActivity.setPremiumButtonVisible(!isPremiumActive); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchaseList) { | ||||||
|  |         if (purchaseList == null || purchaseList.isEmpty()) { | ||||||
|  |             // Premium is not active, or billing is unavailable | ||||||
|  |             updatePremiumState(false); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Purchase premiumPurchase = null; | ||||||
|  |         for (Purchase purchase : purchaseList) { | ||||||
|  |             if (purchase.getSku().equals(BILLING_SKU_PREMIUM)) { | ||||||
|  |                 premiumPurchase = purchase; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (premiumPurchase != null && premiumPurchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { | ||||||
|  |             // Premium has been purchased | ||||||
|  |             updatePremiumState(true); | ||||||
|  | 
 | ||||||
|  |             // Acknowledge the purchase if it hasn't already been acknowledged. | ||||||
|  |             if (!premiumPurchase.isAcknowledged()) { | ||||||
|  |                 AcknowledgePurchaseParams acknowledgePurchaseParams = | ||||||
|  |                         AcknowledgePurchaseParams.newBuilder() | ||||||
|  |                                 .setPurchaseToken(premiumPurchase.getPurchaseToken()) | ||||||
|  |                                 .build(); | ||||||
|  | 
 | ||||||
|  |                 AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = billingResult1 -> { | ||||||
|  |                     Toast.makeText(mActivity, R.string.premium_settings_welcome, Toast.LENGTH_SHORT).show(); | ||||||
|  |                 }; | ||||||
|  |                 mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (mUpdateBillingCallback != null) { | ||||||
|  |                 try { | ||||||
|  |                     mUpdateBillingCallback.run(); | ||||||
|  |                 } catch (Exception e) { | ||||||
|  |                     e.printStackTrace(); | ||||||
|  |                 } | ||||||
|  |                 mUpdateBillingCallback = null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void onQuerySkuDetailsFinished(List<SkuDetails> skuDetailsList) { | ||||||
|  |         if (skuDetailsList == null) { | ||||||
|  |             // This can happen when no user is signed in | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (skuDetailsList.isEmpty()) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         mSkuPremium = skuDetailsList.get(0); | ||||||
|  | 
 | ||||||
|  |         queryPurchases(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void querySkuDetails() { | ||||||
|  |         Runnable queryToExecute = new Runnable() { | ||||||
|  |             @Override | ||||||
|  |             public void run() { | ||||||
|  |                 SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); | ||||||
|  |                 List<String> skuList = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  |                 skuList.add(BILLING_SKU_PREMIUM); | ||||||
|  |                 params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP); | ||||||
|  | 
 | ||||||
|  |                 mBillingClient.querySkuDetailsAsync(params.build(), | ||||||
|  |                         (billingResult, skuDetailsList) -> onQuerySkuDetailsFinished(skuDetailsList)); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         executeServiceRequest(queryToExecute); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void onQueryPurchasesFinished(PurchasesResult result) { | ||||||
|  |         // Have we been disposed of in the meantime? If so, or bad result code, then quit | ||||||
|  |         if (mBillingClient == null || result.getResponseCode() != BillingClient.BillingResponseCode.OK) { | ||||||
|  |             updatePremiumState(false); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         // Update the UI and purchases inventory with new list of purchases | ||||||
|  |         onPurchasesUpdated(result.getBillingResult(), result.getPurchasesList()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void queryPurchases() { | ||||||
|  |         Runnable queryToExecute = new Runnable() { | ||||||
|  |             @Override | ||||||
|  |             public void run() { | ||||||
|  |                 final PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP); | ||||||
|  |                 onQueryPurchasesFinished(purchasesResult); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         executeServiceRequest(queryToExecute); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void startServiceConnection(final Runnable executeOnFinish) { | ||||||
|  |         mBillingClient.startConnection(new BillingClientStateListener() { | ||||||
|  |             @Override | ||||||
|  |             public void onBillingSetupFinished(BillingResult billingResult) { | ||||||
|  |                 if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { | ||||||
|  |                     mIsServiceConnected = true; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (executeOnFinish != null) { | ||||||
|  |                     executeOnFinish.run(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onBillingServiceDisconnected() { | ||||||
|  |                 mIsServiceConnected = false; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void executeServiceRequest(Runnable runnable) { | ||||||
|  |         if (mIsServiceConnected) { | ||||||
|  |             runnable.run(); | ||||||
|  |         } else { | ||||||
|  |             // If billing service was disconnected, we try to reconnect 1 time. | ||||||
|  |             startServiceConnection(runnable); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,66 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.view.InputDevice; | ||||||
|  | import android.view.KeyEvent; | ||||||
|  | import android.view.MotionEvent; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Some controllers have incorrect mappings. This class has special-case fixes for them. | ||||||
|  |  */ | ||||||
|  | public class ControllerMappingHelper { | ||||||
|  |     /** | ||||||
|  |      * Some controllers report extra button presses that can be ignored. | ||||||
|  |      */ | ||||||
|  |     public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode) { | ||||||
|  |         if (isDualShock4(inputDevice)) { | ||||||
|  |             // The two analog triggers generate analog motion events as well as a keycode. | ||||||
|  |             // We always prefer to use the analog values, so throw away the button press | ||||||
|  |             return keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Scale an axis to be zero-centered with a proper range. | ||||||
|  |      */ | ||||||
|  |     public float scaleAxis(InputDevice inputDevice, int axis, float value) { | ||||||
|  |         if (isDualShock4(inputDevice)) { | ||||||
|  |             // Android doesn't have correct mappings for this controller's triggers. It reports them | ||||||
|  |             // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0] | ||||||
|  |             // Scale them to properly zero-centered with a range of [0.0, 1.0]. | ||||||
|  |             if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) { | ||||||
|  |                 return (value + 1) / 2.0f; | ||||||
|  |             } | ||||||
|  |         } else if (isXboxOneWireless(inputDevice)) { | ||||||
|  |             // Same as the DualShock 4, the mappings are missing. | ||||||
|  |             if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) { | ||||||
|  |                 return (value + 1) / 2.0f; | ||||||
|  |             } | ||||||
|  |             if (axis == MotionEvent.AXIS_GENERIC_1) { | ||||||
|  |                 // This axis is stuck at ~.5. Ignore it. | ||||||
|  |                 return 0.0f; | ||||||
|  |             } | ||||||
|  |         } else if (isMogaPro2Hid(inputDevice)) { | ||||||
|  |             // This controller has a broken axis that reports a constant value. Ignore it. | ||||||
|  |             if (axis == MotionEvent.AXIS_GENERIC_1) { | ||||||
|  |                 return 0.0f; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean isDualShock4(InputDevice inputDevice) { | ||||||
|  |         // Sony DualShock 4 controller | ||||||
|  |         return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean isXboxOneWireless(InputDevice inputDevice) { | ||||||
|  |         // Microsoft Xbox One controller | ||||||
|  |         return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean isMogaPro2Hid(InputDevice inputDevice) { | ||||||
|  |         // Moga Pro 2 HID | ||||||
|  |         return inputDevice.getVendorId() == 0x20d6 && inputDevice.getProductId() == 0x6271; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,186 @@ | ||||||
|  | /** | ||||||
|  |  * Copyright 2014 Dolphin Emulator Project | ||||||
|  |  * Licensed under GPLv2+ | ||||||
|  |  * Refer to the license.txt file included. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.os.Environment; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | 
 | ||||||
|  | import androidx.localbroadcastmanager.content.LocalBroadcastManager; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.FileOutputStream; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.InputStream; | ||||||
|  | import java.io.OutputStream; | ||||||
|  | import java.util.concurrent.atomic.AtomicBoolean; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A service that spawns its own thread in order to copy several binary and shader files | ||||||
|  |  * from the Citra APK to the external file system. | ||||||
|  |  */ | ||||||
|  | public final class DirectoryInitialization { | ||||||
|  |     public static final String BROADCAST_ACTION = "org.citra.citra_emu.BROADCAST"; | ||||||
|  | 
 | ||||||
|  |     public static final String EXTRA_STATE = "directoryState"; | ||||||
|  |     private static volatile DirectoryInitializationState directoryState = null; | ||||||
|  |     private static String userPath; | ||||||
|  |     private static AtomicBoolean isCitraDirectoryInitializationRunning = new AtomicBoolean(false); | ||||||
|  | 
 | ||||||
|  |     public static void start(Context context) { | ||||||
|  |         // Can take a few seconds to run, so don't block UI thread. | ||||||
|  |         //noinspection TrivialFunctionalExpressionUsage | ||||||
|  |         ((Runnable) () -> init(context)).run(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void init(Context context) { | ||||||
|  |         if (!isCitraDirectoryInitializationRunning.compareAndSet(false, true)) | ||||||
|  |             return; | ||||||
|  | 
 | ||||||
|  |         if (directoryState != DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) { | ||||||
|  |             if (PermissionsHandler.hasWriteAccess(context)) { | ||||||
|  |                 if (setCitraUserDirectory()) { | ||||||
|  |                     initializeInternalStorage(context); | ||||||
|  |                     NativeLibrary.CreateConfigFile(); | ||||||
|  |                     directoryState = DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED; | ||||||
|  |                 } else { | ||||||
|  |                     directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE; | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         isCitraDirectoryInitializationRunning.set(false); | ||||||
|  |         sendBroadcastState(directoryState, context); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void deleteDirectoryRecursively(File file) { | ||||||
|  |         if (file.isDirectory()) { | ||||||
|  |             for (File child : file.listFiles()) | ||||||
|  |                 deleteDirectoryRecursively(child); | ||||||
|  |         } | ||||||
|  |         file.delete(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static boolean areCitraDirectoriesReady() { | ||||||
|  |         return directoryState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static String getUserDirectory() { | ||||||
|  |         if (directoryState == null) { | ||||||
|  |             throw new IllegalStateException("DirectoryInitialization has to run at least once!"); | ||||||
|  |         } else if (isCitraDirectoryInitializationRunning.get()) { | ||||||
|  |             throw new IllegalStateException( | ||||||
|  |                     "DirectoryInitialization has to finish running first!"); | ||||||
|  |         } | ||||||
|  |         return userPath; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static native void SetSysDirectory(String path); | ||||||
|  | 
 | ||||||
|  |     private static boolean setCitraUserDirectory() { | ||||||
|  |         if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { | ||||||
|  |             File externalPath = Environment.getExternalStorageDirectory(); | ||||||
|  |             if (externalPath != null) { | ||||||
|  |                 userPath = externalPath.getAbsolutePath() + "/citra-emu"; | ||||||
|  |                 Log.debug("[DirectoryInitialization] User Dir: " + userPath); | ||||||
|  |                 // NativeLibrary.SetUserDirectory(userPath); | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void initializeInternalStorage(Context context) { | ||||||
|  |         File sysDirectory = new File(context.getFilesDir(), "Sys"); | ||||||
|  | 
 | ||||||
|  |         SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); | ||||||
|  |         String revision = NativeLibrary.GetGitRevision(); | ||||||
|  |         if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) { | ||||||
|  |             // There is no extracted Sys directory, or there is a Sys directory from another | ||||||
|  |             // version of Citra that might contain outdated files. Let's (re-)extract Sys. | ||||||
|  |             deleteDirectoryRecursively(sysDirectory); | ||||||
|  |             copyAssetFolder("Sys", sysDirectory, true, context); | ||||||
|  | 
 | ||||||
|  |             SharedPreferences.Editor editor = preferences.edit(); | ||||||
|  |             editor.putString("sysDirectoryVersion", revision); | ||||||
|  |             editor.apply(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Let the native code know where the Sys directory is. | ||||||
|  |         SetSysDirectory(sysDirectory.getPath()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void sendBroadcastState(DirectoryInitializationState state, Context context) { | ||||||
|  |         Intent localIntent = | ||||||
|  |                 new Intent(BROADCAST_ACTION) | ||||||
|  |                         .putExtra(EXTRA_STATE, state); | ||||||
|  |         LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void copyAsset(String asset, File output, Boolean overwrite, Context context) { | ||||||
|  |         Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             if (!output.exists() || overwrite) { | ||||||
|  |                 InputStream in = context.getAssets().open(asset); | ||||||
|  |                 OutputStream out = new FileOutputStream(output); | ||||||
|  |                 copyFile(in, out); | ||||||
|  |                 in.close(); | ||||||
|  |                 out.close(); | ||||||
|  |             } | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset + | ||||||
|  |                     e.getMessage()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite, | ||||||
|  |                                         Context context) { | ||||||
|  |         Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " + | ||||||
|  |                 outputFolder); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             boolean createdFolder = false; | ||||||
|  |             for (String file : context.getAssets().list(assetFolder)) { | ||||||
|  |                 if (!createdFolder) { | ||||||
|  |                     outputFolder.mkdir(); | ||||||
|  |                     createdFolder = true; | ||||||
|  |                 } | ||||||
|  |                 copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file), | ||||||
|  |                         overwrite, context); | ||||||
|  |                 copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite, | ||||||
|  |                         context); | ||||||
|  |             } | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder + | ||||||
|  |                     e.getMessage()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static void copyFile(InputStream in, OutputStream out) throws IOException { | ||||||
|  |         byte[] buffer = new byte[1024]; | ||||||
|  |         int read; | ||||||
|  | 
 | ||||||
|  |         while ((read = in.read(buffer)) != -1) { | ||||||
|  |             out.write(buffer, 0, read); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public enum DirectoryInitializationState { | ||||||
|  |         CITRA_DIRECTORIES_INITIALIZED, | ||||||
|  |         EXTERNAL_STORAGE_PERMISSION_NEEDED, | ||||||
|  |         CANT_FIND_EXTERNAL_STORAGE | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.content.BroadcastReceiver; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState; | ||||||
|  | 
 | ||||||
|  | public class DirectoryStateReceiver extends BroadcastReceiver { | ||||||
|  |     Action1<DirectoryInitializationState> callback; | ||||||
|  | 
 | ||||||
|  |     public DirectoryStateReceiver(Action1<DirectoryInitializationState> callback) { | ||||||
|  |         this.callback = callback; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onReceive(Context context, Intent intent) { | ||||||
|  |         DirectoryInitializationState state = (DirectoryInitializationState) intent | ||||||
|  |                 .getSerializableExtra(DirectoryInitialization.EXTRA_STATE); | ||||||
|  |         callback.call(state); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,78 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | 
 | ||||||
|  | public class EmulationMenuSettings { | ||||||
|  |     private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); | ||||||
|  | 
 | ||||||
|  |     // These must match what is defined in src/core/settings.h | ||||||
|  |     public static final int LayoutOption_Default = 0; | ||||||
|  |     public static final int LayoutOption_SingleScreen = 1; | ||||||
|  |     public static final int LayoutOption_LargeScreen = 2; | ||||||
|  |     public static final int LayoutOption_SideScreen = 3; | ||||||
|  |     public static final int LayoutOption_MobilePortrait = 4; | ||||||
|  |     public static final int LayoutOption_MobileLandscape = 5; | ||||||
|  | 
 | ||||||
|  |     public static boolean getJoystickRelCenter() { | ||||||
|  |         return mPreferences.getBoolean("EmulationMenuSettings_JoystickRelCenter", true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void setJoystickRelCenter(boolean value) { | ||||||
|  |         final SharedPreferences.Editor editor = mPreferences.edit(); | ||||||
|  |         editor.putBoolean("EmulationMenuSettings_JoystickRelCenter", value); | ||||||
|  |         editor.apply(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static boolean getDpadSlideEnable() { | ||||||
|  |         return mPreferences.getBoolean("EmulationMenuSettings_DpadSlideEnable", true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void setDpadSlideEnable(boolean value) { | ||||||
|  |         final SharedPreferences.Editor editor = mPreferences.edit(); | ||||||
|  |         editor.putBoolean("EmulationMenuSettings_DpadSlideEnable", value); | ||||||
|  |         editor.apply(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static int getLandscapeScreenLayout() { | ||||||
|  |         return mPreferences.getInt("EmulationMenuSettings_LandscapeScreenLayout", LayoutOption_MobileLandscape); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void setLandscapeScreenLayout(int value) { | ||||||
|  |         final SharedPreferences.Editor editor = mPreferences.edit(); | ||||||
|  |         editor.putInt("EmulationMenuSettings_LandscapeScreenLayout", value); | ||||||
|  |         editor.apply(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static boolean getShowFps() { | ||||||
|  |         return mPreferences.getBoolean("EmulationMenuSettings_ShowFps", false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void setShowFps(boolean value) { | ||||||
|  |         final SharedPreferences.Editor editor = mPreferences.edit(); | ||||||
|  |         editor.putBoolean("EmulationMenuSettings_ShowFps", value); | ||||||
|  |         editor.apply(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static boolean getSwapScreens() { | ||||||
|  |         return mPreferences.getBoolean("EmulationMenuSettings_SwapScreens", false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void setSwapScreens(boolean value) { | ||||||
|  |         final SharedPreferences.Editor editor = mPreferences.edit(); | ||||||
|  |         editor.putBoolean("EmulationMenuSettings_SwapScreens", value); | ||||||
|  |         editor.apply(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static boolean getShowOverlay() { | ||||||
|  |         return mPreferences.getBoolean("EmulationMenuSettings_ShowOverylay", true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void setShowOverlay(boolean value) { | ||||||
|  |         final SharedPreferences.Editor editor = mPreferences.edit(); | ||||||
|  |         editor.putBoolean("EmulationMenuSettings_ShowOverylay", value); | ||||||
|  |         editor.apply(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,73 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.os.Environment; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.fragment.app.FragmentActivity; | ||||||
|  | 
 | ||||||
|  | import com.nononsenseapps.filepicker.FilePickerActivity; | ||||||
|  | import com.nononsenseapps.filepicker.Utils; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.activities.CustomFilePickerActivity; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | public final class FileBrowserHelper { | ||||||
|  |     public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) { | ||||||
|  |         Intent i = new Intent(activity, CustomFilePickerActivity.class); | ||||||
|  | 
 | ||||||
|  |         i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); | ||||||
|  |         i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); | ||||||
|  |         i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR); | ||||||
|  |         i.putExtra(FilePickerActivity.EXTRA_START_PATH, | ||||||
|  |                 Environment.getExternalStorageDirectory().getPath()); | ||||||
|  |         i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); | ||||||
|  |         i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); | ||||||
|  | 
 | ||||||
|  |         activity.startActivityForResult(i, requestCode); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void openFilePicker(FragmentActivity activity, int requestCode, int title, | ||||||
|  |                                       List<String> extensions, boolean allowMultiple) { | ||||||
|  |         Intent i = new Intent(activity, CustomFilePickerActivity.class); | ||||||
|  | 
 | ||||||
|  |         i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMultiple); | ||||||
|  |         i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); | ||||||
|  |         i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); | ||||||
|  |         i.putExtra(FilePickerActivity.EXTRA_START_PATH, | ||||||
|  |                 Environment.getExternalStorageDirectory().getPath()); | ||||||
|  |         i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); | ||||||
|  |         i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); | ||||||
|  | 
 | ||||||
|  |         activity.startActivityForResult(i, requestCode); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public static String getSelectedDirectory(Intent result) { | ||||||
|  |         // Use the provided utility method to parse the result | ||||||
|  |         List<Uri> files = Utils.getSelectedFilesFromResult(result); | ||||||
|  |         if (!files.isEmpty()) { | ||||||
|  |             File file = Utils.getFileForUri(files.get(0)); | ||||||
|  |             return file.getAbsolutePath(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public static String[] getSelectedFiles(Intent result) { | ||||||
|  |         // Use the provided utility method to parse the result | ||||||
|  |         List<Uri> files = Utils.getSelectedFilesFromResult(result); | ||||||
|  |         if (!files.isEmpty()) { | ||||||
|  |             String[] paths = new String[files.size()]; | ||||||
|  |             for (int i = 0; i < files.size(); i++) | ||||||
|  |                 paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath(); | ||||||
|  |             return paths; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,37 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import java.io.File; | ||||||
|  | import java.io.FileInputStream; | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.io.InputStream; | ||||||
|  | 
 | ||||||
|  | public class FileUtil { | ||||||
|  |     public static byte[] getBytesFromFile(File file) throws IOException { | ||||||
|  |         final long length = file.length(); | ||||||
|  | 
 | ||||||
|  |         // You cannot create an array using a long type. | ||||||
|  |         if (length > Integer.MAX_VALUE) { | ||||||
|  |             // File is too large | ||||||
|  |             throw new IOException("File is too large!"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         byte[] bytes = new byte[(int) length]; | ||||||
|  | 
 | ||||||
|  |         int offset = 0; | ||||||
|  |         int numRead; | ||||||
|  | 
 | ||||||
|  |         try (InputStream is = new FileInputStream(file)) { | ||||||
|  |             while (offset < bytes.length | ||||||
|  |                     && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { | ||||||
|  |                 offset += numRead; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Ensure all the bytes have been read in | ||||||
|  |         if (offset < bytes.length) { | ||||||
|  |             throw new IOException("Could not completely read file " + file.getName()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return bytes; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | /** | ||||||
|  |  * Copyright 2014 Dolphin Emulator Project | ||||||
|  |  * Licensed under GPLv2+ | ||||||
|  |  * Refer to the license.txt file included. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.app.PendingIntent; | ||||||
|  | import android.app.Service; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.os.IBinder; | ||||||
|  | 
 | ||||||
|  | import androidx.core.app.NotificationCompat; | ||||||
|  | import androidx.core.app.NotificationManagerCompat; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.activities.EmulationActivity; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A service that shows a permanent notification in the background to avoid the app getting | ||||||
|  |  * cleared from memory by the system. | ||||||
|  |  */ | ||||||
|  | public class ForegroundService extends Service { | ||||||
|  |     private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000; | ||||||
|  | 
 | ||||||
|  |     private void showRunningNotification() { | ||||||
|  |         // Intent is used to resume emulation if the notification is clicked | ||||||
|  |         PendingIntent contentIntent = PendingIntent.getActivity(this, 0, | ||||||
|  |                 new Intent(this, EmulationActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); | ||||||
|  | 
 | ||||||
|  |         NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id)) | ||||||
|  |                 .setSmallIcon(R.drawable.ic_stat_notification_logo) | ||||||
|  |                 .setContentTitle(getString(R.string.app_name)) | ||||||
|  |                 .setContentText(getString(R.string.app_notification_running)) | ||||||
|  |                 .setPriority(NotificationCompat.PRIORITY_LOW) | ||||||
|  |                 .setOngoing(true) | ||||||
|  |                 .setVibrate(null) | ||||||
|  |                 .setSound(null) | ||||||
|  |                 .setContentIntent(contentIntent); | ||||||
|  |         startForeground(EMULATION_RUNNING_NOTIFICATION, builder.build()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public IBinder onBind(Intent intent) { | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onCreate() { | ||||||
|  |         showRunningNotification(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int onStartCommand(Intent intent, int flags, int startId) { | ||||||
|  |         return START_STICKY; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onDestroy() { | ||||||
|  |         NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | 
 | ||||||
|  | import com.squareup.picasso.Picasso; | ||||||
|  | import com.squareup.picasso.Request; | ||||||
|  | import com.squareup.picasso.RequestHandler; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.NativeLibrary; | ||||||
|  | 
 | ||||||
|  | import java.nio.IntBuffer; | ||||||
|  | 
 | ||||||
|  | public class GameIconRequestHandler extends RequestHandler { | ||||||
|  |     @Override | ||||||
|  |     public boolean canHandleRequest(Request data) { | ||||||
|  |         return "iso".equals(data.uri.getScheme()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result load(Request request, int networkPolicy) { | ||||||
|  |         String url = request.uri.getHost() + request.uri.getPath(); | ||||||
|  |         int[] vector = NativeLibrary.GetIcon(url); | ||||||
|  |         Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565); | ||||||
|  |         bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector)); | ||||||
|  |         return new Result(bitmap, Picasso.LoadedFrom.DISK); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,39 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.BuildConfig; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Contains methods that call through to {@link android.util.Log}, but | ||||||
|  |  * with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log | ||||||
|  |  * levels in release builds. | ||||||
|  |  */ | ||||||
|  | public final class Log { | ||||||
|  |     private static final String TAG = "Citra Frontend"; | ||||||
|  | 
 | ||||||
|  |     private Log() { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void verbose(String message) { | ||||||
|  |         if (BuildConfig.DEBUG) { | ||||||
|  |             android.util.Log.v(TAG, message); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void debug(String message) { | ||||||
|  |         if (BuildConfig.DEBUG) { | ||||||
|  |             android.util.Log.d(TAG, message); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void info(String message) { | ||||||
|  |         android.util.Log.i(TAG, message); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void warning(String message) { | ||||||
|  |         android.util.Log.w(TAG, message); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void error(String message) { | ||||||
|  |         android.util.Log.e(TAG, message); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,35 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.annotation.TargetApi; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.pm.PackageManager; | ||||||
|  | import android.os.Build; | ||||||
|  | 
 | ||||||
|  | import androidx.core.content.ContextCompat; | ||||||
|  | import androidx.fragment.app.FragmentActivity; | ||||||
|  | 
 | ||||||
|  | import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; | ||||||
|  | 
 | ||||||
|  | public class PermissionsHandler { | ||||||
|  |     public static final int REQUEST_CODE_WRITE_PERMISSION = 500; | ||||||
|  | 
 | ||||||
|  |     // We use permissions acceptance as an indicator if this is a first boot for the user. | ||||||
|  |     public static boolean isFirstBoot(final FragmentActivity activity) { | ||||||
|  |         return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @TargetApi(Build.VERSION_CODES.M) | ||||||
|  |     public static boolean checkWritePermission(final FragmentActivity activity) { | ||||||
|  |         if (isFirstBoot(activity)) { | ||||||
|  |             activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, | ||||||
|  |                     REQUEST_CODE_WRITE_PERMISSION); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static boolean hasWriteAccess(Context context) { | ||||||
|  |         return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,45 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | import android.graphics.BitmapShader; | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.graphics.Paint; | ||||||
|  | import android.graphics.Rect; | ||||||
|  | import android.graphics.RectF; | ||||||
|  | 
 | ||||||
|  | import com.squareup.picasso.Transformation; | ||||||
|  | 
 | ||||||
|  | public class PicassoRoundedCornersTransformation implements Transformation { | ||||||
|  |     @Override | ||||||
|  |     public Bitmap transform(Bitmap icon) { | ||||||
|  |         final int width = icon.getWidth(); | ||||||
|  |         final int height = icon.getHeight(); | ||||||
|  |         final Rect rect = new Rect(0, 0, width, height); | ||||||
|  |         final int size = Math.min(width, height); | ||||||
|  |         final int x = (width - size) / 2; | ||||||
|  |         final int y = (height - size) / 2; | ||||||
|  | 
 | ||||||
|  |         Bitmap squaredBitmap = Bitmap.createBitmap(icon, x, y, size, size); | ||||||
|  |         if (squaredBitmap != icon) { | ||||||
|  |             icon.recycle(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); | ||||||
|  |         Canvas canvas = new Canvas(output); | ||||||
|  |         BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP); | ||||||
|  |         Paint paint = new Paint(); | ||||||
|  |         paint.setAntiAlias(true); | ||||||
|  |         paint.setShader(shader); | ||||||
|  | 
 | ||||||
|  |         canvas.drawRoundRect(new RectF(rect), 10, 10, paint); | ||||||
|  | 
 | ||||||
|  |         squaredBitmap.recycle(); | ||||||
|  | 
 | ||||||
|  |         return output; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public String key() { | ||||||
|  |         return "circle"; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,57 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.graphics.Bitmap; | ||||||
|  | import android.net.Uri; | ||||||
|  | import android.widget.ImageView; | ||||||
|  | 
 | ||||||
|  | import com.squareup.picasso.Picasso; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
|  | public class PicassoUtils { | ||||||
|  |     private static boolean mPicassoInitialized = false; | ||||||
|  | 
 | ||||||
|  |     public static void init() { | ||||||
|  |         if (mPicassoInitialized) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         Picasso picassoInstance = new Picasso.Builder(CitraApplication.getAppContext()) | ||||||
|  |                 .addRequestHandler(new GameIconRequestHandler()) | ||||||
|  |                 .build(); | ||||||
|  | 
 | ||||||
|  |         Picasso.setSingletonInstance(picassoInstance); | ||||||
|  |         mPicassoInitialized = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void loadGameIcon(ImageView imageView, String gamePath) { | ||||||
|  |         Picasso | ||||||
|  |                 .get() | ||||||
|  |                 .load(Uri.parse("iso:/" + gamePath)) | ||||||
|  |                 .fit() | ||||||
|  |                 .centerInside() | ||||||
|  |                 .config(Bitmap.Config.RGB_565) | ||||||
|  |                 .error(R.drawable.no_icon) | ||||||
|  |                 .transform(new PicassoRoundedCornersTransformation()) | ||||||
|  |                 .into(imageView); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Blocking call. Load image from file and crop/resize it to fit in width x height. | ||||||
|  |     @Nullable | ||||||
|  |     public static Bitmap LoadBitmapFromFile(String uri, int width, int height) { | ||||||
|  |         try { | ||||||
|  |             return Picasso.get() | ||||||
|  |                     .load(Uri.parse(uri)) | ||||||
|  |                     .config(Bitmap.Config.ARGB_8888) | ||||||
|  |                     .centerCrop() | ||||||
|  |                     .resize(width, height) | ||||||
|  |                     .get(); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,45 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.text.TextUtils; | ||||||
|  | 
 | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | import androidx.fragment.app.FragmentActivity; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | import org.citra.citra_emu.activities.EmulationActivity; | ||||||
|  | 
 | ||||||
|  | public final class StartupHandler { | ||||||
|  |     private static void handlePermissionsCheck(FragmentActivity parent) { | ||||||
|  |         // Ask the user to grant write permission if it's not already granted | ||||||
|  |         PermissionsHandler.checkWritePermission(parent); | ||||||
|  | 
 | ||||||
|  |         String start_file = ""; | ||||||
|  |         Bundle extras = parent.getIntent().getExtras(); | ||||||
|  |         if (extras != null) { | ||||||
|  |             start_file = extras.getString("AutoStartFile"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!TextUtils.isEmpty(start_file)) { | ||||||
|  |             // Start the emulation activity, send the ISO passed in and finish the main activity | ||||||
|  |             Intent emulation_intent = new Intent(parent, EmulationActivity.class); | ||||||
|  |             emulation_intent.putExtra("SelectedGame", start_file); | ||||||
|  |             parent.startActivity(emulation_intent); | ||||||
|  |             parent.finish(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void HandleInit(FragmentActivity parent) { | ||||||
|  |         if (PermissionsHandler.isFirstBoot(parent)) { | ||||||
|  |             // Prompt user with standard first boot disclaimer | ||||||
|  |             new AlertDialog.Builder(parent) | ||||||
|  |                     .setTitle(R.string.app_name) | ||||||
|  |                     .setIcon(R.mipmap.ic_launcher) | ||||||
|  |                     .setMessage(parent.getResources().getString(R.string.app_disclaimer)) | ||||||
|  |                     .setPositiveButton(android.R.string.ok, null) | ||||||
|  |                     .setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent)) | ||||||
|  |                     .show(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,34 @@ | ||||||
|  | package org.citra.citra_emu.utils; | ||||||
|  | 
 | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.os.Build; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | 
 | ||||||
|  | import androidx.appcompat.app.AppCompatDelegate; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.CitraApplication; | ||||||
|  | import org.citra.citra_emu.features.settings.utils.SettingsFile; | ||||||
|  | 
 | ||||||
|  | public class ThemeUtil { | ||||||
|  |     private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); | ||||||
|  | 
 | ||||||
|  |     private static void applyTheme(int designValue) { | ||||||
|  |         switch (designValue) { | ||||||
|  |             case 0: | ||||||
|  |                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); | ||||||
|  |                 break; | ||||||
|  |             case 1: | ||||||
|  |                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); | ||||||
|  |                 break; | ||||||
|  |             case 2: | ||||||
|  |                 AppCompatDelegate.setDefaultNightMode(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? | ||||||
|  |                         AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM : | ||||||
|  |                         AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static void applyTheme() { | ||||||
|  |         applyTheme(mPreferences.getInt(SettingsFile.KEY_DESIGN, 0)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,46 @@ | ||||||
|  | package org.citra.citra_emu.viewholders; | ||||||
|  | 
 | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.ImageView; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  | 
 | ||||||
|  | import org.citra.citra_emu.R; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A simple class that stores references to views so that the GameAdapter doesn't need to | ||||||
|  |  * keep calling findViewById(), which is expensive. | ||||||
|  |  */ | ||||||
|  | public class GameViewHolder extends RecyclerView.ViewHolder { | ||||||
|  |     private View itemView; | ||||||
|  |     public ImageView imageIcon; | ||||||
|  |     public TextView textGameTitle; | ||||||
|  |     public TextView textCompany; | ||||||
|  |     public TextView textFileName; | ||||||
|  | 
 | ||||||
|  |     public String gameId; | ||||||
|  | 
 | ||||||
|  |     // TODO Not need any of this stuff. Currently only the properties dialog needs it. | ||||||
|  |     public String path; | ||||||
|  |     public String title; | ||||||
|  |     public String description; | ||||||
|  |     public String regions; | ||||||
|  |     public String company; | ||||||
|  | 
 | ||||||
|  |     public GameViewHolder(View itemView) { | ||||||
|  |         super(itemView); | ||||||
|  | 
 | ||||||
|  |         this.itemView = itemView; | ||||||
|  |         itemView.setTag(this); | ||||||
|  | 
 | ||||||
|  |         imageIcon = itemView.findViewById(R.id.image_game_screen); | ||||||
|  |         textGameTitle = itemView.findViewById(R.id.text_game_title); | ||||||
|  |         textCompany = itemView.findViewById(R.id.text_company); | ||||||
|  |         textFileName = itemView.findViewById(R.id.text_filename); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public View getItemView() { | ||||||
|  |         return itemView; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,13 +0,0 @@ | ||||||
| // Copyright 2018 Citra Emulator Project |  | ||||||
| // Licensed under GPLv2 or any later version |  | ||||||
| // Refer to the license.txt file included. |  | ||||||
| 
 |  | ||||||
| package org.citra_emu.citra; |  | ||||||
| 
 |  | ||||||
| import android.app.Application; |  | ||||||
| 
 |  | ||||||
| public class CitraApplication extends Application { |  | ||||||
|     static { |  | ||||||
|         System.loadLibrary("citra-android"); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue