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 | ||||
|         env: | ||||
|           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: | ||||
|     runs-on: ubuntu-latest | ||||
|     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) | ||||
| 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 | ||||
| # ======================================================================= | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										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 | ||||
| project_type: android | ||||
| trigger_map: | ||||
|  | @ -7,118 +7,83 @@ trigger_map: | |||
|   workflow: primary | ||||
| - pull_request_source_branch: "*" | ||||
|   workflow: primary | ||||
| - tag: "*" | ||||
|   workflow: deploy | ||||
| workflows: | ||||
|   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: | ||||
|     - cache-pull@2.4.0: {} | ||||
|     - script@1.1.6: | ||||
|     - activate-ssh-key@4: {} | ||||
|     - git-clone@6: {} | ||||
|     - cache-pull@2: {} | ||||
|     - script@1: | ||||
|         title: Install newer cmake | ||||
|         inputs: | ||||
|             - content: |- | ||||
|                 #!/bin/bash | ||||
|                 set -ex | ||||
|                 sudo apt remove cmake -y | ||||
|                 sudo apt purge --auto-remove cmake -y | ||||
|                 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: | ||||
|         - content: |- | ||||
|             #!/bin/bash | ||||
|             set -ex | ||||
|             sdkmanager --install "cmake;3.18.1" | ||||
|     - install-missing-android-tools@2.3: | ||||
|         inputs: | ||||
|         - gradlew_path: "$PROJECT_LOCATION/gradlew" | ||||
|     - change-android-versioncode-and-versionname@1.1.1: | ||||
|         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: | ||||
|     - android-lint@0: | ||||
|         inputs: | ||||
|         - project_location: "$PROJECT_LOCATION" | ||||
|         - module: "$MODULE" | ||||
|         - 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 ""}}' | ||||
|     - deploy-to-bitrise-io@1.11.1: {} | ||||
|     - cache-push@2.4.1: {} | ||||
|     - bitrise-step-export-universal-apk@0: | ||||
|         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: | ||||
|     steps: | ||||
|     - cache-pull@2.4.0: {} | ||||
|     - script@1.1.6: | ||||
|         title: Install newer cmake | ||||
|     - activate-ssh-key@4: {} | ||||
|     - git-clone@6: {} | ||||
|     - cache-pull@2: {} | ||||
|     - script@1: | ||||
|         title: Deps | ||||
|         inputs: | ||||
|             - content: |- | ||||
|                 #!/bin/bash | ||||
|                 set -ex | ||||
|                 sudo apt remove cmake -y | ||||
|                 sudo apt purge --auto-remove cmake -y | ||||
|                 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: | ||||
|         - content: |- | ||||
|             #!/bin/bash | ||||
|             set -ex | ||||
|             sdkmanager --install "cmake;3.18.1" | ||||
|     - install-missing-android-tools@3: | ||||
|         inputs: | ||||
|             - gradlew_path: "$PROJECT_LOCATION/gradlew" | ||||
|     - android-lint@0.9.8: | ||||
|         - gradlew_path: "$PROJECT_LOCATION/gradlew" | ||||
|     - android-lint@0: | ||||
|         inputs: | ||||
|         - project_location: "$PROJECT_LOCATION" | ||||
|         - module: "$MODULE" | ||||
|         - variant: "$TEST_VARIANT" | ||||
|     - android-build@0.10.3: | ||||
|         - variant: "$BUILD_VARIANT" | ||||
|     - android-build@1: | ||||
|         inputs: | ||||
|         - variant: Debug | ||||
|         - variant: "$BUILD_VARIANT" | ||||
|         - project_location: "$PROJECT_LOCATION" | ||||
|     - deploy-to-bitrise-io@1.11.1: {} | ||||
|     - cache-push@2.4.1: {} | ||||
|         - build_type: apk | ||||
|     - cache-push@2: {} | ||||
|     - deploy-to-bitrise-io@2: {} | ||||
| meta: | ||||
|   bitrise.io: | ||||
|     stack: linux-docker-android-20.04 | ||||
| app: | ||||
|   envs: | ||||
|   - opts: | ||||
|  | @ -132,4 +97,3 @@ app: | |||
|     BUILD_VARIANT: Release | ||||
|   - opts: | ||||
|       is_expand: false | ||||
|     TEST_VARIANT: Debug | ||||
|  |  | |||
|  | @ -117,8 +117,10 @@ endif() | |||
| if (ENABLE_QT) | ||||
|     add_subdirectory(citra_qt) | ||||
| endif() | ||||
| 
 | ||||
| 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() | ||||
|     add_subdirectory(dedicated_room) | ||||
| 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 | ||||
| .gradle | ||||
| /local.properties | ||||
| /.idea/libraries | ||||
| /.idea/modules.xml | ||||
| /.idea/workspace.xml | ||||
| .DS_Store | ||||
| /build | ||||
| /captures | ||||
| .idea/ | ||||
| 
 | ||||
| # Keystore files | ||||
| # Uncomment the following line if you do not want to check your keystore files in. | ||||
| #*.jks | ||||
| 
 | ||||
| # External native build folder generated in Android Studio 2.2 and later | ||||
| .externalNativeBuild | ||||
| 
 | ||||
| # CXX compile cache | ||||
|  |  | |||
|  | @ -1,8 +1,17 @@ | |||
| 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 { | ||||
|     compileSdkVersion 26 | ||||
|     buildToolsVersion '28.0.3' | ||||
|     compileSdkVersion 29 | ||||
|     ndkVersion "23.1.7779620" | ||||
| 
 | ||||
|     compileOptions { | ||||
|         sourceCompatibility JavaVersion.VERSION_1_8 | ||||
|  | @ -13,34 +22,54 @@ android { | |||
|         // This is important as it will run lint but not abort on error | ||||
|         // Lint has some overly obnoxious "errors" that should really be warnings | ||||
|         abortOnError false | ||||
| 
 | ||||
|         //Uncomment disable lines for test builds... | ||||
|         //disable 'MissingTranslation'bin | ||||
|         //disable 'ExtraTranslation' | ||||
|     } | ||||
| 
 | ||||
|     defaultConfig { | ||||
|         applicationId "org.citra_emu" | ||||
|         minSdkVersion 21 | ||||
|         targetSdkVersion 26 | ||||
| 
 | ||||
|         versionCode(getBuildVersionCode()) | ||||
| 
 | ||||
|         versionName "${getVersion()}" | ||||
|         // TODO If this is ever modified, change application_id in strings.xml | ||||
|         applicationId "org.citra.citra_emu" | ||||
|         minSdkVersion 26 | ||||
|         targetSdkVersion 29 | ||||
|         versionCode autoVersion | ||||
|         versionName getVersion() | ||||
|         ndk.abiFilters abiFilter | ||||
|     } | ||||
| 
 | ||||
|     signingConfigs { | ||||
|         release { | ||||
|             if (project.hasProperty('keystore')) { | ||||
|                 storeFile file(project.property('keystore')) | ||||
|                 storePassword project.property('storepass') | ||||
|                 keyAlias project.property('keyalias') | ||||
|                 keyPassword project.property('keypass') | ||||
|             } | ||||
|         } | ||||
|         //release { | ||||
|         //    storeFile file('') | ||||
|         //    storePassword System.getenv('ANDROID_KEYPASS') | ||||
|         //    keyAlias = 'key0' | ||||
|         //    keyPassword System.getenv('ANDROID_KEYPASS') | ||||
|         //} | ||||
|     } | ||||
| 
 | ||||
|     applicationVariants.all { variant -> | ||||
|         buildType = variant.buildType.name // sets the current build type | ||||
|     } | ||||
| 
 | ||||
|     // Define build types, which are orthogonal to product flavors. | ||||
|     buildTypes { | ||||
| 
 | ||||
|         // Signed by release key, allowing for upload to Play Store. | ||||
|         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. | ||||
|  | @ -49,13 +78,14 @@ android { | |||
|             // TODO If this is ever modified, change application_id in debug/strings.xml | ||||
|             applicationIdSuffix ".debug" | ||||
|             versionNameSuffix '-debug' | ||||
|             debuggable true | ||||
|             jniDebuggable true | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     externalNativeBuild { | ||||
|         cmake { | ||||
|             version getCmakeVersion() | ||||
|             version "3.18.1" | ||||
|             path "../../../CMakeLists.txt" | ||||
|         } | ||||
|     } | ||||
|  | @ -65,76 +95,46 @@ android { | |||
|             cmake { | ||||
|                 arguments "-DENABLE_QT=0", // Don't use QT | ||||
|                         "-DENABLE_SDL2=0", // Don't use SDL | ||||
|                         "-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work | ||||
|                         "-DENABLE_CUBEB=0", | ||||
|                         "-DANDROID_STL=c++_shared" | ||||
|                         "-DENABLE_WEB_SERVICE=0", // Don't use telemetry | ||||
|                         "-DANDROID_ARM_NEON=true" // cryptopp requires Neon to work | ||||
| 
 | ||||
|                 abiFilters "arm64-v8a" | ||||
| 
 | ||||
|                 targets "citra-android" | ||||
|                 abiFilters abiFilter | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ext { | ||||
|     androidSupportVersion = '26.1.0' | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
|     implementation "com.android.support:support-v13:$androidSupportVersion" | ||||
|     implementation "com.android.support:cardview-v7:$androidSupportVersion" | ||||
|     implementation "com.android.support:recyclerview-v7:$androidSupportVersion" | ||||
|     implementation "com.android.support:design:$androidSupportVersion" | ||||
|     implementation 'androidx.appcompat:appcompat:1.1.0' | ||||
|     implementation 'androidx.exifinterface:exifinterface:1.2.0' | ||||
|     implementation 'androidx.cardview:cardview:1.0.0' | ||||
|     implementation 'androidx.recyclerview:recyclerview:1.1.0' | ||||
|     implementation 'com.google.android.material:material:1.1.0' | ||||
| 
 | ||||
|     // Android TV UI libraries. | ||||
|     implementation "com.android.support:leanback-v17:$androidSupportVersion" | ||||
|     // For loading huge screenshots from the disk. | ||||
|     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" | ||||
|     androidTestImplementation "com.android.support.test:runner:1.0.1" | ||||
|     implementation 'com.android.billingclient:billing:2.0.3' | ||||
| } | ||||
| 
 | ||||
| def getVersion() { | ||||
|     def versionNumber = '0.0' | ||||
|     def versionName = '0.0' | ||||
| 
 | ||||
|     try { | ||||
|         versionNumber = 'git describe --always --long'.execute([], project.rootDir).text | ||||
|         versionName = 'git describe --always --long'.execute([], project.rootDir).text | ||||
|                 .trim() | ||||
|                 .replaceAll(/(-0)?-[^-]+$/, "") | ||||
|     } catch (Exception e) { | ||||
|     } catch (Exception) { | ||||
|         logger.error('Cannot find git, defaulting to dummy version number') | ||||
|     } | ||||
| 
 | ||||
|     return versionNumber | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 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 | ||||
|     return versionName | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| package org.citra_emu.citra; | ||||
| package org.citra.citra_emu; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.support.test.InstrumentationRegistry; | ||||
|  | @ -21,6 +21,6 @@ public class ExampleInstrumentedTest { | |||
|         // Context of the app under test. | ||||
|         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"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     package="org.citra_emu.citra"> | ||||
|     package="org.citra.citra_emu"> | ||||
|     <uses-feature | ||||
|         android:name="android.hardware.touchscreen" | ||||
|         android:required="false"/> | ||||
| 
 | ||||
|     <uses-feature | ||||
|         android:name="android.hardware.gamepad" | ||||
|         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.CAMERA" /> | ||||
|     <uses-permission android:name="android.permission.RECORD_AUDIO" /> | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||
| 
 | ||||
| 
 | ||||
|     <application | ||||
|         android:name="org.citra_emu.citra.CitraApplication" | ||||
|         android:label="Citra" | ||||
|         android:icon="@mipmap/ic_citra" | ||||
|         android:allowBackup="true" | ||||
|         android:name="org.citra.citra_emu.CitraApplication" | ||||
|         android:label="@string/app_name" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:allowBackup="false" | ||||
|         android:supportsRtl="true" | ||||
|         android:isGame="true" | ||||
|         android:banner="@mipmap/ic_citra"> | ||||
|         android:banner="@mipmap/ic_launcher" | ||||
|         android:requestLegacyExternalStorage="true"> | ||||
| 
 | ||||
|         <activity | ||||
|             android:name=".ui.main.MainActivity" | ||||
|             android:theme="@style/CitraBase"> | ||||
|             android:name="org.citra.citra_emu.ui.main.MainActivity" | ||||
|             android:theme="@style/CitraBase" | ||||
|             android:resizeableActivity="false"> | ||||
| 
 | ||||
|             <!-- This intentfilter marks this Activity as the one that gets launched from Home screen. --> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN"/> | ||||
|                 <action android:name="android.intent.action.VIEW"/> | ||||
| 
 | ||||
|                 <category android:name="android.intent.category.LAUNCHER"/> | ||||
|             </intent-filter> | ||||
|         </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> | ||||
| 
 | ||||
| </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