mirror of
				https://git.suyu.dev/suyu/suyu
				synced 2025-11-04 00:49:02 -06:00 
			
		
		
		
	android: Add Citra frontend.
This commit is contained in:
		
							
								
								
									
										62
									
								
								src/android/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/android/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
# 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
 | 
			
		||||
.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
 | 
			
		||||
app/.cxx
 | 
			
		||||
 | 
			
		||||
# Google Services (e.g. APIs or Firebase)
 | 
			
		||||
google-services.json
 | 
			
		||||
 | 
			
		||||
# Freeline
 | 
			
		||||
freeline.py
 | 
			
		||||
freeline/
 | 
			
		||||
freeline_project_description.json
 | 
			
		||||
 | 
			
		||||
# fastlane
 | 
			
		||||
fastlane/report.xml
 | 
			
		||||
fastlane/Preview.html
 | 
			
		||||
fastlane/screenshots
 | 
			
		||||
fastlane/test_output
 | 
			
		||||
fastlane/readme.md
 | 
			
		||||
							
								
								
									
										163
									
								
								src/android/app/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src/android/app/build.gradle
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,163 @@
 | 
			
		||||
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 32
 | 
			
		||||
    ndkVersion "25.1.8937393"
 | 
			
		||||
 | 
			
		||||
    compileOptions {
 | 
			
		||||
        sourceCompatibility JavaVersion.VERSION_1_8
 | 
			
		||||
        targetCompatibility JavaVersion.VERSION_1_8
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    lintOptions {
 | 
			
		||||
        // 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 {
 | 
			
		||||
        // TODO If this is ever modified, change application_id in strings.xml
 | 
			
		||||
        applicationId "org.citra.citra_emu"
 | 
			
		||||
        minSdkVersion 28
 | 
			
		||||
        targetSdkVersion 29
 | 
			
		||||
        versionCode autoVersion
 | 
			
		||||
        versionName getVersion()
 | 
			
		||||
        ndk.abiFilters abiFilter
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    signingConfigs {
 | 
			
		||||
        //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.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.
 | 
			
		||||
        // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build.
 | 
			
		||||
        debug {
 | 
			
		||||
            // TODO If this is ever modified, change application_id in debug/strings.xml
 | 
			
		||||
            applicationIdSuffix ".debug"
 | 
			
		||||
            versionNameSuffix '-debug'
 | 
			
		||||
            debuggable true
 | 
			
		||||
            jniDebuggable true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    flavorDimensions "version"
 | 
			
		||||
    productFlavors {
 | 
			
		||||
        canary {
 | 
			
		||||
            dimension "version"
 | 
			
		||||
            applicationIdSuffix ".canary"
 | 
			
		||||
        }
 | 
			
		||||
        nightly {
 | 
			
		||||
            dimension "version"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    externalNativeBuild {
 | 
			
		||||
        cmake {
 | 
			
		||||
            version "3.22.1"
 | 
			
		||||
            path "../../../CMakeLists.txt"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    defaultConfig {
 | 
			
		||||
        externalNativeBuild {
 | 
			
		||||
            cmake {
 | 
			
		||||
                arguments "-DENABLE_QT=0", // Don't use QT
 | 
			
		||||
                        "-DENABLE_SDL2=0", // Don't use SDL
 | 
			
		||||
                        "-DENABLE_WEB_SERVICE=0", // Don't use telemetry
 | 
			
		||||
                        "-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
 | 
			
		||||
                        "-DYUZU_USE_BUNDLED_VCPKG=ON",
 | 
			
		||||
                        "-DYUZU_USE_BUNDLED_FFMPEG=ON"
 | 
			
		||||
 | 
			
		||||
                abiFilters abiFilter
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    implementation 'androidx.appcompat:appcompat:1.5.1'
 | 
			
		||||
    implementation 'androidx.exifinterface:exifinterface:1.3.4'
 | 
			
		||||
    implementation 'androidx.cardview:cardview:1.0.0'
 | 
			
		||||
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
 | 
			
		||||
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
 | 
			
		||||
    implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
 | 
			
		||||
    implementation 'androidx.fragment:fragment:1.5.3'
 | 
			
		||||
    implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
 | 
			
		||||
    implementation 'com.google.android.material:material:1.6.1'
 | 
			
		||||
 | 
			
		||||
    // For loading huge screenshots from the disk.
 | 
			
		||||
    implementation 'com.squareup.picasso:picasso:2.71828'
 | 
			
		||||
 | 
			
		||||
    // 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:2.1.4'
 | 
			
		||||
    implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
 | 
			
		||||
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
 | 
			
		||||
 | 
			
		||||
    // Please don't upgrade the billing library as the newer version is not GPL-compatible
 | 
			
		||||
    implementation 'com.android.billingclient:billing:2.0.3'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def getVersion() {
 | 
			
		||||
    def versionName = '0.0'
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        versionName = 'git describe --always --long'.execute([], project.rootDir).text
 | 
			
		||||
                .trim()
 | 
			
		||||
                .replaceAll(/(-0)?-[^-]+$/, "")
 | 
			
		||||
    } catch (Exception) {
 | 
			
		||||
        logger.error('Cannot find git, defaulting to dummy version number')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (System.getenv("GITHUB_ACTIONS") != null) {
 | 
			
		||||
        def gitTag = System.getenv("GIT_TAG_NAME")
 | 
			
		||||
        versionName = gitTag ?: versionName
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return versionName
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/android/app/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/android/app/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
# Add project specific ProGuard rules here.
 | 
			
		||||
# You can control the set of applied configuration files using the
 | 
			
		||||
# proguardFiles setting in build.gradle.
 | 
			
		||||
#
 | 
			
		||||
# For more details, see
 | 
			
		||||
#   http://developer.android.com/guide/developing/tools/proguard.html
 | 
			
		||||
 | 
			
		||||
# If your project uses WebView with JS, uncomment the following
 | 
			
		||||
# and specify the fully qualified class name to the JavaScript interface
 | 
			
		||||
# class:
 | 
			
		||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
 | 
			
		||||
#   public *;
 | 
			
		||||
#}
 | 
			
		||||
 | 
			
		||||
# Uncomment this to preserve the line number information for
 | 
			
		||||
# debugging stack traces.
 | 
			
		||||
#-keepattributes SourceFile,LineNumberTable
 | 
			
		||||
 | 
			
		||||
# If you keep the line number information, uncomment this to
 | 
			
		||||
# hide the original source file name.
 | 
			
		||||
#-renamesourcefileattribute SourceFile
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
package org.citra.citra_emu;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
							
								
								
									
										99
									
								
								src/android/app/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/android/app/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    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="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.INTERNET" />
 | 
			
		||||
    <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.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_launcher"
 | 
			
		||||
        android:requestLegacyExternalStorage="true">
 | 
			
		||||
 | 
			
		||||
        <activity
 | 
			
		||||
            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"/>
 | 
			
		||||
 | 
			
		||||
                <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"
 | 
			
		||||
            android:screenOrientation="landscape"/>
 | 
			
		||||
 | 
			
		||||
        <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>
 | 
			
		||||
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name="org.citra.citra_emu.features.cheats.ui.CheatsActivity"
 | 
			
		||||
            android:exported="false"
 | 
			
		||||
            android:theme="@style/CitraSettingsBase"
 | 
			
		||||
            android:label="@string/cheats"/>
 | 
			
		||||
 | 
			
		||||
        <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>
 | 
			
		||||
@@ -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,631 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.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.utils.EmulationMenuSettings;
 | 
			
		||||
import org.citra.citra_emu.utils.Log;
 | 
			
		||||
 | 
			
		||||
import java.lang.ref.WeakReference;
 | 
			
		||||
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("yuzu-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);
 | 
			
		||||
 | 
			
		||||
    // Create the config.ini file.
 | 
			
		||||
    public static native void CreateConfigFile();
 | 
			
		||||
 | 
			
		||||
    public static native int DefaultCPUCore();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Begins emulation.
 | 
			
		||||
     */
 | 
			
		||||
    public static native void Run(String path);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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);
 | 
			
		||||
 | 
			
		||||
    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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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 = 0;
 | 
			
		||||
        public static final int BUTTON_B = 1;
 | 
			
		||||
        public static final int BUTTON_X = 2;
 | 
			
		||||
        public static final int BUTTON_Y = 3;
 | 
			
		||||
        public static final int BUTTON_START = 11;
 | 
			
		||||
        public static final int BUTTON_SELECT = 12;
 | 
			
		||||
        public static final int BUTTON_HOME = 19;
 | 
			
		||||
        public static final int BUTTON_ZL = 9;
 | 
			
		||||
        public static final int BUTTON_ZR = 10;
 | 
			
		||||
        public static final int DPAD_UP = 14;
 | 
			
		||||
        public static final int DPAD_DOWN = 16;
 | 
			
		||||
        public static final int DPAD_LEFT = 13;
 | 
			
		||||
        public static final int DPAD_RIGHT = 15;
 | 
			
		||||
        public static final int STICK_LEFT = 5;
 | 
			
		||||
        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 = 6;
 | 
			
		||||
        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 = 7;
 | 
			
		||||
        public static final int TRIGGER_R = 8;
 | 
			
		||||
        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,755 @@
 | 
			
		||||
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.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.cheats.ui.CheatsActivity;
 | 
			
		||||
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 MENU_ACTION_OPEN_CHEATS = 17;
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
        buttonsActionsMap
 | 
			
		||||
                .append(R.id.menu_emulation_open_cheats, EmulationActivity.MENU_ACTION_OPEN_CHEATS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @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);
 | 
			
		||||
        menu.findItem(R.id.menu_emulation_save_state).setVisible(false);
 | 
			
		||||
        menu.findItem(R.id.menu_emulation_load_state).setVisible(false);
 | 
			
		||||
        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);
 | 
			
		||||
                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;
 | 
			
		||||
 | 
			
		||||
            case MENU_ACTION_OPEN_CHEATS:
 | 
			
		||||
                CheatsActivity.launch(this);
 | 
			
		||||
                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);
 | 
			
		||||
        } 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() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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,122 @@
 | 
			
		||||
// 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.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.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,138 @@
 | 
			
		||||
// 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.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,57 @@
 | 
			
		||||
package org.citra.citra_emu.features.cheats.model;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Keep;
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
public class Cheat {
 | 
			
		||||
    @Keep
 | 
			
		||||
    private final long mPointer;
 | 
			
		||||
 | 
			
		||||
    private Runnable mEnabledChangedCallback = null;
 | 
			
		||||
 | 
			
		||||
    @Keep
 | 
			
		||||
    private Cheat(long pointer) {
 | 
			
		||||
        mPointer = pointer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected native void finalize();
 | 
			
		||||
 | 
			
		||||
    @NonNull
 | 
			
		||||
    public native String getName();
 | 
			
		||||
 | 
			
		||||
    @NonNull
 | 
			
		||||
    public native String getNotes();
 | 
			
		||||
 | 
			
		||||
    @NonNull
 | 
			
		||||
    public native String getCode();
 | 
			
		||||
 | 
			
		||||
    public native boolean getEnabled();
 | 
			
		||||
 | 
			
		||||
    public void setEnabled(boolean enabled) {
 | 
			
		||||
        setEnabledImpl(enabled);
 | 
			
		||||
        onEnabledChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private native void setEnabledImpl(boolean enabled);
 | 
			
		||||
 | 
			
		||||
    public void setEnabledChangedCallback(@Nullable Runnable callback) {
 | 
			
		||||
        mEnabledChangedCallback = callback;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onEnabledChanged() {
 | 
			
		||||
        if (mEnabledChangedCallback != null) {
 | 
			
		||||
            mEnabledChangedCallback.run();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * If the code is valid, returns 0. Otherwise, returns the 1-based index
 | 
			
		||||
     * for the line containing the error.
 | 
			
		||||
     */
 | 
			
		||||
    public static native int isValidGatewayCode(@NonNull String code);
 | 
			
		||||
 | 
			
		||||
    public static native Cheat createGatewayCode(@NonNull String name, @NonNull String notes,
 | 
			
		||||
                                                 @NonNull String code);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
package org.citra.citra_emu.features.cheats.model;
 | 
			
		||||
 | 
			
		||||
public class CheatEngine {
 | 
			
		||||
    public static native Cheat[] getCheats();
 | 
			
		||||
 | 
			
		||||
    public static native void addCheat(Cheat cheat);
 | 
			
		||||
 | 
			
		||||
    public static native void removeCheat(int index);
 | 
			
		||||
 | 
			
		||||
    public static native void updateCheat(int index, Cheat newCheat);
 | 
			
		||||
 | 
			
		||||
    public static native void saveCheatFile();
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,177 @@
 | 
			
		||||
package org.citra.citra_emu.features.cheats.model;
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.LiveData;
 | 
			
		||||
import androidx.lifecycle.MutableLiveData;
 | 
			
		||||
import androidx.lifecycle.ViewModel;
 | 
			
		||||
 | 
			
		||||
public class CheatsViewModel extends ViewModel {
 | 
			
		||||
    private int mSelectedCheatPosition = -1;
 | 
			
		||||
    private final MutableLiveData<Cheat> mSelectedCheat = new MutableLiveData<>(null);
 | 
			
		||||
    private final MutableLiveData<Boolean> mIsAdding = new MutableLiveData<>(false);
 | 
			
		||||
    private final MutableLiveData<Boolean> mIsEditing = new MutableLiveData<>(false);
 | 
			
		||||
 | 
			
		||||
    private final MutableLiveData<Integer> mCheatAddedEvent = new MutableLiveData<>(null);
 | 
			
		||||
    private final MutableLiveData<Integer> mCheatChangedEvent = new MutableLiveData<>(null);
 | 
			
		||||
    private final MutableLiveData<Integer> mCheatDeletedEvent = new MutableLiveData<>(null);
 | 
			
		||||
    private final MutableLiveData<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
 | 
			
		||||
 | 
			
		||||
    private Cheat[] mCheats;
 | 
			
		||||
    private boolean mCheatsNeedSaving = false;
 | 
			
		||||
 | 
			
		||||
    public void load() {
 | 
			
		||||
        mCheats = CheatEngine.getCheats();
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < mCheats.length; i++) {
 | 
			
		||||
            int position = i;
 | 
			
		||||
            mCheats[i].setEnabledChangedCallback(() -> {
 | 
			
		||||
                mCheatsNeedSaving = true;
 | 
			
		||||
                notifyCheatUpdated(position);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void saveIfNeeded() {
 | 
			
		||||
        if (mCheatsNeedSaving) {
 | 
			
		||||
            CheatEngine.saveCheatFile();
 | 
			
		||||
            mCheatsNeedSaving = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Cheat[] getCheats() {
 | 
			
		||||
        return mCheats;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public LiveData<Cheat> getSelectedCheat() {
 | 
			
		||||
        return mSelectedCheat;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setSelectedCheat(Cheat cheat, int position) {
 | 
			
		||||
        if (mIsEditing.getValue()) {
 | 
			
		||||
            setIsEditing(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mSelectedCheat.setValue(cheat);
 | 
			
		||||
        mSelectedCheatPosition = position;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public LiveData<Boolean> getIsAdding() {
 | 
			
		||||
        return mIsAdding;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public LiveData<Boolean> getIsEditing() {
 | 
			
		||||
        return mIsEditing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setIsEditing(boolean isEditing) {
 | 
			
		||||
        mIsEditing.setValue(isEditing);
 | 
			
		||||
 | 
			
		||||
        if (mIsAdding.getValue() && !isEditing) {
 | 
			
		||||
            mIsAdding.setValue(false);
 | 
			
		||||
            setSelectedCheat(null, -1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * When a cheat is added, the integer stored in the returned LiveData
 | 
			
		||||
     * changes to the position of that cheat, then changes back to null.
 | 
			
		||||
     */
 | 
			
		||||
    public LiveData<Integer> getCheatAddedEvent() {
 | 
			
		||||
        return mCheatAddedEvent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void notifyCheatAdded(int position) {
 | 
			
		||||
        mCheatAddedEvent.setValue(position);
 | 
			
		||||
        mCheatAddedEvent.setValue(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void startAddingCheat() {
 | 
			
		||||
        mSelectedCheat.setValue(null);
 | 
			
		||||
        mSelectedCheatPosition = -1;
 | 
			
		||||
 | 
			
		||||
        mIsAdding.setValue(true);
 | 
			
		||||
        mIsEditing.setValue(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void finishAddingCheat(Cheat cheat) {
 | 
			
		||||
        if (!mIsAdding.getValue()) {
 | 
			
		||||
            throw new IllegalStateException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mIsAdding.setValue(false);
 | 
			
		||||
        mIsEditing.setValue(false);
 | 
			
		||||
 | 
			
		||||
        int position = mCheats.length;
 | 
			
		||||
 | 
			
		||||
        CheatEngine.addCheat(cheat);
 | 
			
		||||
 | 
			
		||||
        mCheatsNeedSaving = true;
 | 
			
		||||
        load();
 | 
			
		||||
 | 
			
		||||
        notifyCheatAdded(position);
 | 
			
		||||
        setSelectedCheat(mCheats[position], position);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * When a cheat is edited, the integer stored in the returned LiveData
 | 
			
		||||
     * changes to the position of that cheat, then changes back to null.
 | 
			
		||||
     */
 | 
			
		||||
    public LiveData<Integer> getCheatUpdatedEvent() {
 | 
			
		||||
        return mCheatChangedEvent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Notifies that an edit has been made to the contents of the cheat at the given position.
 | 
			
		||||
     */
 | 
			
		||||
    private void notifyCheatUpdated(int position) {
 | 
			
		||||
        mCheatChangedEvent.setValue(position);
 | 
			
		||||
        mCheatChangedEvent.setValue(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void updateSelectedCheat(Cheat newCheat) {
 | 
			
		||||
        CheatEngine.updateCheat(mSelectedCheatPosition, newCheat);
 | 
			
		||||
 | 
			
		||||
        mCheatsNeedSaving = true;
 | 
			
		||||
        load();
 | 
			
		||||
 | 
			
		||||
        notifyCheatUpdated(mSelectedCheatPosition);
 | 
			
		||||
        setSelectedCheat(mCheats[mSelectedCheatPosition], mSelectedCheatPosition);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * When a cheat is deleted, the integer stored in the returned LiveData
 | 
			
		||||
     * changes to the position of that cheat, then changes back to null.
 | 
			
		||||
     */
 | 
			
		||||
    public LiveData<Integer> getCheatDeletedEvent() {
 | 
			
		||||
        return mCheatDeletedEvent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Notifies that the cheat at the given position has been deleted.
 | 
			
		||||
     */
 | 
			
		||||
    private void notifyCheatDeleted(int position) {
 | 
			
		||||
        mCheatDeletedEvent.setValue(position);
 | 
			
		||||
        mCheatDeletedEvent.setValue(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void deleteSelectedCheat() {
 | 
			
		||||
        int position = mSelectedCheatPosition;
 | 
			
		||||
 | 
			
		||||
        setSelectedCheat(null, -1);
 | 
			
		||||
 | 
			
		||||
        CheatEngine.removeCheat(position);
 | 
			
		||||
 | 
			
		||||
        mCheatsNeedSaving = true;
 | 
			
		||||
        load();
 | 
			
		||||
 | 
			
		||||
        notifyCheatDeleted(position);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public LiveData<Boolean> getOpenDetailsViewEvent() {
 | 
			
		||||
        return mOpenDetailsViewEvent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void openDetailsView() {
 | 
			
		||||
        mOpenDetailsViewEvent.setValue(true);
 | 
			
		||||
        mOpenDetailsViewEvent.setValue(false);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,174 @@
 | 
			
		||||
package org.citra.citra_emu.features.cheats.ui;
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
import android.widget.Button;
 | 
			
		||||
import android.widget.EditText;
 | 
			
		||||
import android.widget.ScrollView;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.fragment.app.Fragment;
 | 
			
		||||
import androidx.lifecycle.ViewModelProvider;
 | 
			
		||||
 | 
			
		||||
import org.citra.citra_emu.R;
 | 
			
		||||
import org.citra.citra_emu.features.cheats.model.Cheat;
 | 
			
		||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
 | 
			
		||||
 | 
			
		||||
public class CheatDetailsFragment extends Fragment {
 | 
			
		||||
    private View mRoot;
 | 
			
		||||
    private ScrollView mScrollView;
 | 
			
		||||
    private TextView mLabelName;
 | 
			
		||||
    private EditText mEditName;
 | 
			
		||||
    private EditText mEditNotes;
 | 
			
		||||
    private EditText mEditCode;
 | 
			
		||||
    private Button mButtonDelete;
 | 
			
		||||
    private Button mButtonEdit;
 | 
			
		||||
    private Button mButtonCancel;
 | 
			
		||||
    private Button mButtonOk;
 | 
			
		||||
 | 
			
		||||
    private CheatsViewModel mViewModel;
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Override
 | 
			
		||||
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
 | 
			
		||||
                             @Nullable Bundle savedInstanceState) {
 | 
			
		||||
        return inflater.inflate(R.layout.fragment_cheat_details, container, false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
 | 
			
		||||
        mRoot = view.findViewById(R.id.root);
 | 
			
		||||
        mScrollView = view.findViewById(R.id.scroll_view);
 | 
			
		||||
        mLabelName = view.findViewById(R.id.label_name);
 | 
			
		||||
        mEditName = view.findViewById(R.id.edit_name);
 | 
			
		||||
        mEditNotes = view.findViewById(R.id.edit_notes);
 | 
			
		||||
        mEditCode = view.findViewById(R.id.edit_code);
 | 
			
		||||
        mButtonDelete = view.findViewById(R.id.button_delete);
 | 
			
		||||
        mButtonEdit = view.findViewById(R.id.button_edit);
 | 
			
		||||
        mButtonCancel = view.findViewById(R.id.button_cancel);
 | 
			
		||||
        mButtonOk = view.findViewById(R.id.button_ok);
 | 
			
		||||
 | 
			
		||||
        CheatsActivity activity = (CheatsActivity) requireActivity();
 | 
			
		||||
        mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
 | 
			
		||||
 | 
			
		||||
        mViewModel.getSelectedCheat().observe(getViewLifecycleOwner(),
 | 
			
		||||
                this::onSelectedCheatUpdated);
 | 
			
		||||
        mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated);
 | 
			
		||||
 | 
			
		||||
        mButtonDelete.setOnClickListener(this::onDeleteClicked);
 | 
			
		||||
        mButtonEdit.setOnClickListener(this::onEditClicked);
 | 
			
		||||
        mButtonCancel.setOnClickListener(this::onCancelClicked);
 | 
			
		||||
        mButtonOk.setOnClickListener(this::onOkClicked);
 | 
			
		||||
 | 
			
		||||
        // On a portrait phone screen (or other narrow screen), only one of the two panes are shown
 | 
			
		||||
        // at the same time. If the user is navigating using a d-pad and moves focus to an element
 | 
			
		||||
        // in the currently hidden pane, we need to manually show that pane.
 | 
			
		||||
        CheatsActivity.setOnFocusChangeListenerRecursively(view,
 | 
			
		||||
                (v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void clearEditErrors() {
 | 
			
		||||
        mEditName.setError(null);
 | 
			
		||||
        mEditCode.setError(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onDeleteClicked(View view) {
 | 
			
		||||
        String name = mEditName.getText().toString();
 | 
			
		||||
 | 
			
		||||
        AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
 | 
			
		||||
        builder.setMessage(getString(R.string.cheats_delete_confirmation, name));
 | 
			
		||||
        builder.setPositiveButton(android.R.string.yes,
 | 
			
		||||
                (dialog, i) -> mViewModel.deleteSelectedCheat());
 | 
			
		||||
        builder.setNegativeButton(android.R.string.no, null);
 | 
			
		||||
        builder.show();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onEditClicked(View view) {
 | 
			
		||||
        mViewModel.setIsEditing(true);
 | 
			
		||||
        mButtonOk.requestFocus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onCancelClicked(View view) {
 | 
			
		||||
        mViewModel.setIsEditing(false);
 | 
			
		||||
        onSelectedCheatUpdated(mViewModel.getSelectedCheat().getValue());
 | 
			
		||||
        mButtonDelete.requestFocus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onOkClicked(View view) {
 | 
			
		||||
        clearEditErrors();
 | 
			
		||||
 | 
			
		||||
        String name = mEditName.getText().toString();
 | 
			
		||||
        String notes = mEditNotes.getText().toString();
 | 
			
		||||
        String code = mEditCode.getText().toString();
 | 
			
		||||
 | 
			
		||||
        if (name.isEmpty()) {
 | 
			
		||||
            mEditName.setError(getString(R.string.cheats_error_no_name));
 | 
			
		||||
            mScrollView.smoothScrollTo(0, mLabelName.getTop());
 | 
			
		||||
            return;
 | 
			
		||||
        } else if (code.isEmpty()) {
 | 
			
		||||
            mEditCode.setError(getString(R.string.cheats_error_no_code_lines));
 | 
			
		||||
            mScrollView.smoothScrollTo(0, mEditCode.getBottom());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        int validityResult = Cheat.isValidGatewayCode(code);
 | 
			
		||||
 | 
			
		||||
        if (validityResult != 0) {
 | 
			
		||||
            mEditCode.setError(getString(R.string.cheats_error_on_line, validityResult));
 | 
			
		||||
            mScrollView.smoothScrollTo(0, mEditCode.getBottom());
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Cheat newCheat = Cheat.createGatewayCode(name, notes, code);
 | 
			
		||||
 | 
			
		||||
        if (mViewModel.getIsAdding().getValue()) {
 | 
			
		||||
            mViewModel.finishAddingCheat(newCheat);
 | 
			
		||||
        } else {
 | 
			
		||||
            mViewModel.updateSelectedCheat(newCheat);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mButtonEdit.requestFocus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onSelectedCheatUpdated(@Nullable Cheat cheat) {
 | 
			
		||||
        clearEditErrors();
 | 
			
		||||
 | 
			
		||||
        boolean isEditing = mViewModel.getIsEditing().getValue();
 | 
			
		||||
 | 
			
		||||
        mRoot.setVisibility(isEditing || cheat != null ? View.VISIBLE : View.GONE);
 | 
			
		||||
 | 
			
		||||
        // If the fragment was recreated while editing a cheat, it's vital that we
 | 
			
		||||
        // don't repopulate the fields, otherwise the user's changes will be lost
 | 
			
		||||
        if (!isEditing) {
 | 
			
		||||
            if (cheat == null) {
 | 
			
		||||
                mEditName.setText("");
 | 
			
		||||
                mEditNotes.setText("");
 | 
			
		||||
                mEditCode.setText("");
 | 
			
		||||
            } else {
 | 
			
		||||
                mEditName.setText(cheat.getName());
 | 
			
		||||
                mEditNotes.setText(cheat.getNotes());
 | 
			
		||||
                mEditCode.setText(cheat.getCode());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onIsEditingUpdated(boolean isEditing) {
 | 
			
		||||
        if (isEditing) {
 | 
			
		||||
            mRoot.setVisibility(View.VISIBLE);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mEditName.setEnabled(isEditing);
 | 
			
		||||
        mEditNotes.setEnabled(isEditing);
 | 
			
		||||
        mEditCode.setEnabled(isEditing);
 | 
			
		||||
 | 
			
		||||
        mButtonDelete.setVisibility(isEditing ? View.GONE : View.VISIBLE);
 | 
			
		||||
        mButtonEdit.setVisibility(isEditing ? View.GONE : View.VISIBLE);
 | 
			
		||||
        mButtonCancel.setVisibility(isEditing ? View.VISIBLE : View.GONE);
 | 
			
		||||
        mButtonOk.setVisibility(isEditing ? View.VISIBLE : View.GONE);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
package org.citra.citra_emu.features.cheats.ui;
 | 
			
		||||
 | 
			
		||||
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.lifecycle.ViewModelProvider;
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager;
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView;
 | 
			
		||||
 | 
			
		||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
 | 
			
		||||
 | 
			
		||||
import org.citra.citra_emu.R;
 | 
			
		||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
 | 
			
		||||
import org.citra.citra_emu.ui.DividerItemDecoration;
 | 
			
		||||
 | 
			
		||||
public class CheatListFragment extends Fragment {
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Override
 | 
			
		||||
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
 | 
			
		||||
                             @Nullable Bundle savedInstanceState) {
 | 
			
		||||
        return inflater.inflate(R.layout.fragment_cheat_list, container, false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
 | 
			
		||||
        RecyclerView recyclerView = view.findViewById(R.id.cheat_list);
 | 
			
		||||
        FloatingActionButton fab = view.findViewById(R.id.fab);
 | 
			
		||||
 | 
			
		||||
        CheatsActivity activity = (CheatsActivity) requireActivity();
 | 
			
		||||
        CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
 | 
			
		||||
 | 
			
		||||
        recyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
 | 
			
		||||
        recyclerView.setLayoutManager(new LinearLayoutManager(activity));
 | 
			
		||||
        recyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
 | 
			
		||||
 | 
			
		||||
        fab.setOnClickListener(v -> {
 | 
			
		||||
            viewModel.startAddingCheat();
 | 
			
		||||
            viewModel.openDetailsView();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,56 @@
 | 
			
		||||
package org.citra.citra_emu.features.cheats.ui;
 | 
			
		||||
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.widget.CheckBox;
 | 
			
		||||
import android.widget.CompoundButton;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.lifecycle.ViewModelProvider;
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView;
 | 
			
		||||
 | 
			
		||||
import org.citra.citra_emu.R;
 | 
			
		||||
import org.citra.citra_emu.features.cheats.model.Cheat;
 | 
			
		||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
 | 
			
		||||
 | 
			
		||||
public class CheatViewHolder extends RecyclerView.ViewHolder
 | 
			
		||||
        implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
 | 
			
		||||
    private final View mRoot;
 | 
			
		||||
    private final TextView mName;
 | 
			
		||||
    private final CheckBox mCheckbox;
 | 
			
		||||
 | 
			
		||||
    private CheatsViewModel mViewModel;
 | 
			
		||||
    private Cheat mCheat;
 | 
			
		||||
    private int mPosition;
 | 
			
		||||
 | 
			
		||||
    public CheatViewHolder(@NonNull View itemView) {
 | 
			
		||||
        super(itemView);
 | 
			
		||||
 | 
			
		||||
        mRoot = itemView.findViewById(R.id.root);
 | 
			
		||||
        mName = itemView.findViewById(R.id.text_name);
 | 
			
		||||
        mCheckbox = itemView.findViewById(R.id.checkbox);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void bind(CheatsActivity activity, Cheat cheat, int position) {
 | 
			
		||||
        mCheckbox.setOnCheckedChangeListener(null);
 | 
			
		||||
 | 
			
		||||
        mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
 | 
			
		||||
        mCheat = cheat;
 | 
			
		||||
        mPosition = position;
 | 
			
		||||
 | 
			
		||||
        mName.setText(mCheat.getName());
 | 
			
		||||
        mCheckbox.setChecked(mCheat.getEnabled());
 | 
			
		||||
 | 
			
		||||
        mRoot.setOnClickListener(this);
 | 
			
		||||
        mCheckbox.setOnCheckedChangeListener(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void onClick(View root) {
 | 
			
		||||
        mViewModel.setSelectedCheat(mCheat, mPosition);
 | 
			
		||||
        mViewModel.openDetailsView();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
 | 
			
		||||
        mCheat.setEnabled(isChecked);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,161 @@
 | 
			
		||||
package org.citra.citra_emu.features.cheats.ui;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.view.Menu;
 | 
			
		||||
import android.view.MenuInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity;
 | 
			
		||||
import androidx.core.view.ViewCompat;
 | 
			
		||||
import androidx.lifecycle.ViewModelProvider;
 | 
			
		||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
 | 
			
		||||
 | 
			
		||||
import org.citra.citra_emu.R;
 | 
			
		||||
import org.citra.citra_emu.features.cheats.model.Cheat;
 | 
			
		||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
 | 
			
		||||
import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback;
 | 
			
		||||
 | 
			
		||||
public class CheatsActivity extends AppCompatActivity
 | 
			
		||||
        implements SlidingPaneLayout.PanelSlideListener {
 | 
			
		||||
    private CheatsViewModel mViewModel;
 | 
			
		||||
 | 
			
		||||
    private SlidingPaneLayout mSlidingPaneLayout;
 | 
			
		||||
    private View mCheatList;
 | 
			
		||||
    private View mCheatDetails;
 | 
			
		||||
 | 
			
		||||
    private View mCheatListLastFocus;
 | 
			
		||||
    private View mCheatDetailsLastFocus;
 | 
			
		||||
 | 
			
		||||
    public static void launch(Context context) {
 | 
			
		||||
        Intent intent = new Intent(context, CheatsActivity.class);
 | 
			
		||||
        context.startActivity(intent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
 | 
			
		||||
        mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
 | 
			
		||||
        mViewModel.load();
 | 
			
		||||
 | 
			
		||||
        setContentView(R.layout.activity_cheats);
 | 
			
		||||
 | 
			
		||||
        mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
 | 
			
		||||
        mCheatList = findViewById(R.id.cheat_list);
 | 
			
		||||
        mCheatDetails = findViewById(R.id.cheat_details);
 | 
			
		||||
 | 
			
		||||
        mCheatListLastFocus = mCheatList;
 | 
			
		||||
        mCheatDetailsLastFocus = mCheatDetails;
 | 
			
		||||
 | 
			
		||||
        mSlidingPaneLayout.addPanelSlideListener(this);
 | 
			
		||||
 | 
			
		||||
        getOnBackPressedDispatcher().addCallback(this,
 | 
			
		||||
                new TwoPaneOnBackPressedCallback(mSlidingPaneLayout));
 | 
			
		||||
 | 
			
		||||
        mViewModel.getSelectedCheat().observe(this, this::onSelectedCheatChanged);
 | 
			
		||||
        mViewModel.getIsEditing().observe(this, this::onIsEditingChanged);
 | 
			
		||||
        onSelectedCheatChanged(mViewModel.getSelectedCheat().getValue());
 | 
			
		||||
 | 
			
		||||
        mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView);
 | 
			
		||||
 | 
			
		||||
        // Show "Up" button in the action bar for navigation
 | 
			
		||||
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean onCreateOptionsMenu(Menu menu) {
 | 
			
		||||
        MenuInflater inflater = getMenuInflater();
 | 
			
		||||
        inflater.inflate(R.menu.menu_settings, menu);
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onStop() {
 | 
			
		||||
        super.onStop();
 | 
			
		||||
 | 
			
		||||
        mViewModel.saveIfNeeded();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onPanelSlide(@NonNull View panel, float slideOffset) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onPanelOpened(@NonNull View panel) {
 | 
			
		||||
        boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
 | 
			
		||||
        mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onPanelClosed(@NonNull View panel) {
 | 
			
		||||
        boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
 | 
			
		||||
        mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onIsEditingChanged(boolean isEditing) {
 | 
			
		||||
        if (isEditing) {
 | 
			
		||||
            mSlidingPaneLayout.setLockMode(SlidingPaneLayout.LOCK_MODE_UNLOCKED);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onSelectedCheatChanged(Cheat selectedCheat) {
 | 
			
		||||
        boolean cheatSelected = selectedCheat != null || mViewModel.getIsEditing().getValue();
 | 
			
		||||
 | 
			
		||||
        if (!cheatSelected && mSlidingPaneLayout.isOpen()) {
 | 
			
		||||
            mSlidingPaneLayout.close();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mSlidingPaneLayout.setLockMode(cheatSelected ?
 | 
			
		||||
                SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void onListViewFocusChange(boolean hasFocus) {
 | 
			
		||||
        if (hasFocus) {
 | 
			
		||||
            mCheatListLastFocus = mCheatList.findFocus();
 | 
			
		||||
            if (mCheatListLastFocus == null)
 | 
			
		||||
                throw new NullPointerException();
 | 
			
		||||
 | 
			
		||||
            mSlidingPaneLayout.close();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void onDetailsViewFocusChange(boolean hasFocus) {
 | 
			
		||||
        if (hasFocus) {
 | 
			
		||||
            mCheatDetailsLastFocus = mCheatDetails.findFocus();
 | 
			
		||||
            if (mCheatDetailsLastFocus == null)
 | 
			
		||||
                throw new NullPointerException();
 | 
			
		||||
 | 
			
		||||
            mSlidingPaneLayout.open();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean onSupportNavigateUp() {
 | 
			
		||||
        onBackPressed();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void openDetailsView(boolean open) {
 | 
			
		||||
        if (open) {
 | 
			
		||||
            mSlidingPaneLayout.open();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void setOnFocusChangeListenerRecursively(@NonNull View view,
 | 
			
		||||
                                                           View.OnFocusChangeListener listener) {
 | 
			
		||||
        view.setOnFocusChangeListener(listener);
 | 
			
		||||
 | 
			
		||||
        if (view instanceof ViewGroup) {
 | 
			
		||||
            ViewGroup viewGroup = (ViewGroup) view;
 | 
			
		||||
            for (int i = 0; i < viewGroup.getChildCount(); i++) {
 | 
			
		||||
                View child = viewGroup.getChildAt(i);
 | 
			
		||||
                setOnFocusChangeListenerRecursively(child, listener);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,72 @@
 | 
			
		||||
package org.citra.citra_emu.features.cheats.ui;
 | 
			
		||||
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView;
 | 
			
		||||
 | 
			
		||||
import org.citra.citra_emu.R;
 | 
			
		||||
import org.citra.citra_emu.features.cheats.model.Cheat;
 | 
			
		||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
 | 
			
		||||
 | 
			
		||||
public class CheatsAdapter extends RecyclerView.Adapter<CheatViewHolder> {
 | 
			
		||||
    private final CheatsActivity mActivity;
 | 
			
		||||
    private final CheatsViewModel mViewModel;
 | 
			
		||||
 | 
			
		||||
    public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel) {
 | 
			
		||||
        mActivity = activity;
 | 
			
		||||
        mViewModel = viewModel;
 | 
			
		||||
 | 
			
		||||
        mViewModel.getCheatAddedEvent().observe(activity, (position) -> {
 | 
			
		||||
            if (position != null) {
 | 
			
		||||
                notifyItemInserted(position);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        mViewModel.getCheatUpdatedEvent().observe(activity, (position) -> {
 | 
			
		||||
            if (position != null) {
 | 
			
		||||
                notifyItemChanged(position);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        mViewModel.getCheatDeletedEvent().observe(activity, (position) -> {
 | 
			
		||||
            if (position != null) {
 | 
			
		||||
                notifyItemRemoved(position);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @NonNull
 | 
			
		||||
    @Override
 | 
			
		||||
    public CheatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
 | 
			
		||||
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
 | 
			
		||||
 | 
			
		||||
        View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false);
 | 
			
		||||
        addViewListeners(cheatView);
 | 
			
		||||
        return new CheatViewHolder(cheatView);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onBindViewHolder(@NonNull CheatViewHolder holder, int position) {
 | 
			
		||||
        holder.bind(mActivity, getItemAt(position), position);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getItemCount() {
 | 
			
		||||
        return mViewModel.getCheats().length;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void addViewListeners(View view) {
 | 
			
		||||
        // On a portrait phone screen (or other narrow screen), only one of the two panes are shown
 | 
			
		||||
        // at the same time. If the user is navigating using a d-pad and moves focus to an element
 | 
			
		||||
        // in the currently hidden pane, we need to manually show that pane.
 | 
			
		||||
        CheatsActivity.setOnFocusChangeListenerRecursively(view,
 | 
			
		||||
                (v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Cheat getItemAt(int position) {
 | 
			
		||||
        return mViewModel.getCheats()[position];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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,132 @@
 | 
			
		||||
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_UTILITY = "Utility";
 | 
			
		||||
    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_UTILITY, 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,12 @@
 | 
			
		||||
package org.citra.citra_emu.features.settings.model.view;
 | 
			
		||||
 | 
			
		||||
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,416 @@
 | 
			
		||||
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.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));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //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);
 | 
			
		||||
        SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY);
 | 
			
		||||
        Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES);
 | 
			
		||||
        Setting customTextures = utilitySection.getSetting(SettingsFile.KEY_CUSTOM_TEXTURES);
 | 
			
		||||
        //Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES);
 | 
			
		||||
 | 
			
		||||
        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));
 | 
			
		||||
 | 
			
		||||
        sl.add(new HeaderSetting(null, null, R.string.utility, 0));
 | 
			
		||||
        sl.add(new CheckBoxSetting(SettingsFile.KEY_DUMP_TEXTURES, Settings.SECTION_UTILITY, R.string.dump_textures, R.string.dump_textures_description, false, dumpTextures));
 | 
			
		||||
        sl.add(new CheckBoxSetting(SettingsFile.KEY_CUSTOM_TEXTURES, Settings.SECTION_UTILITY, R.string.custom_textures, R.string.custom_textures_description, false, customTextures));
 | 
			
		||||
        //Disabled until custom texture implementation gets rewrite, current one overloads RAM and crashes Citra.
 | 
			
		||||
        //sl.add(new CheckBoxSetting(SettingsFile.KEY_PRELOAD_TEXTURES, Settings.SECTION_UTILITY, R.string.preload_textures, R.string.preload_textures_description, false, preloadTextures));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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,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.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,341 @@
 | 
			
		||||
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_DUMP_TEXTURES = "dump_textures";
 | 
			
		||||
    public static final String KEY_CUSTOM_TEXTURES = "custom_textures";
 | 
			
		||||
    public static final String KEY_PRELOAD_TEXTURES = "preload_textures";
 | 
			
		||||
 | 
			
		||||
    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,380 @@
 | 
			
		||||
package org.citra.citra_emu.fragments;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.IntentFilter;
 | 
			
		||||
import android.content.SharedPreferences;
 | 
			
		||||
import android.graphics.Color;
 | 
			
		||||
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);
 | 
			
		||||
        mPerfStats.setTextColor(Color.YELLOW);
 | 
			
		||||
 | 
			
		||||
        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 (true) {
 | 
			
		||||
            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]),
 | 
			
		||||
                            (int) (perfStats[SPEED] * 100.0)));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                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,276 @@
 | 
			
		||||
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.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(
 | 
			
		||||
                ".xci", ".nsp", ".nca", ".nro"));
 | 
			
		||||
 | 
			
		||||
        // 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();
 | 
			
		||||
 | 
			
		||||
        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(
 | 
			
		||||
                            ".xci", ".nsp", ".nca", ".nro"));
 | 
			
		||||
                    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,37 @@
 | 
			
		||||
package org.citra.citra_emu.ui;
 | 
			
		||||
 | 
			
		||||
import android.view.View;
 | 
			
		||||
 | 
			
		||||
import androidx.activity.OnBackPressedCallback;
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
 | 
			
		||||
 | 
			
		||||
public class TwoPaneOnBackPressedCallback extends OnBackPressedCallback
 | 
			
		||||
        implements SlidingPaneLayout.PanelSlideListener {
 | 
			
		||||
    private final SlidingPaneLayout mSlidingPaneLayout;
 | 
			
		||||
 | 
			
		||||
    public TwoPaneOnBackPressedCallback(@NonNull SlidingPaneLayout slidingPaneLayout) {
 | 
			
		||||
        super(slidingPaneLayout.isSlideable() && slidingPaneLayout.isOpen());
 | 
			
		||||
        mSlidingPaneLayout = slidingPaneLayout;
 | 
			
		||||
        slidingPaneLayout.addPanelSlideListener(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handleOnBackPressed() {
 | 
			
		||||
        mSlidingPaneLayout.close();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onPanelSlide(@NonNull View panel, float slideOffset) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onPanelOpened(@NonNull View panel) {
 | 
			
		||||
        setEnabled(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onPanelClosed(@NonNull View panel) {
 | 
			
		||||
        setEnabled(false);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,267 @@
 | 
			
		||||
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.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("xci", "nsp", "cci", "3ds",
 | 
			
		||||
                                                                    "cxi", "app", "3dsx", "cia",
 | 
			
		||||
                                                                    "rar", "zip", "7z", "torrent",
 | 
			
		||||
                                                                    "tar", "gz", "nro"));
 | 
			
		||||
                    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) {
 | 
			
		||||
                        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_IMMUTABLE);
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								src/android/app/src/main/res/animator/settings_enter.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/android/app/src/main/res/animator/settings_enter.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/decelerate_cubic"
 | 
			
		||||
        android:propertyName="yFraction"
 | 
			
		||||
        android:startOffset="@android:integer/config_shortAnimTime"
 | 
			
		||||
        android:valueFrom="1.0"
 | 
			
		||||
        android:valueTo="0" />
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/decelerate_cubic"
 | 
			
		||||
        android:propertyName="translationZ"
 | 
			
		||||
        android:startOffset="@android:integer/config_shortAnimTime"
 | 
			
		||||
        android:valueFrom="100.0"
 | 
			
		||||
        android:valueTo="0" />
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/decelerate_cubic"
 | 
			
		||||
        android:propertyName="elevation"
 | 
			
		||||
        android:startOffset="@android:integer/config_shortAnimTime"
 | 
			
		||||
        android:valueFrom="100.0"
 | 
			
		||||
        android:valueTo="0" />
 | 
			
		||||
 | 
			
		||||
</set>
 | 
			
		||||
							
								
								
									
										28
									
								
								src/android/app/src/main/res/animator/settings_exit.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/android/app/src/main/res/animator/settings_exit.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/accelerate_cubic"
 | 
			
		||||
        android:propertyName="visibleness"
 | 
			
		||||
        android:valueFrom="1.0f"
 | 
			
		||||
        android:valueTo="0.6f"
 | 
			
		||||
        android:valueType="floatType" />
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/decelerate_cubic"
 | 
			
		||||
        android:propertyName="translationZ"
 | 
			
		||||
        android:startOffset="@android:integer/config_shortAnimTime"
 | 
			
		||||
        android:valueFrom="0"
 | 
			
		||||
        android:valueTo="-100.0" />
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/decelerate_cubic"
 | 
			
		||||
        android:propertyName="elevation"
 | 
			
		||||
        android:startOffset="@android:integer/config_shortAnimTime"
 | 
			
		||||
        android:valueFrom="0"
 | 
			
		||||
        android:valueTo="-100.0" />
 | 
			
		||||
 | 
			
		||||
</set>
 | 
			
		||||
							
								
								
									
										28
									
								
								src/android/app/src/main/res/animator/settings_pop_enter.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/android/app/src/main/res/animator/settings_pop_enter.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/decelerate_cubic"
 | 
			
		||||
        android:propertyName="visibleness"
 | 
			
		||||
        android:valueFrom="0.6f"
 | 
			
		||||
        android:valueTo="1.0f"
 | 
			
		||||
        android:valueType="floatType" />
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/decelerate_cubic"
 | 
			
		||||
        android:propertyName="translationZ"
 | 
			
		||||
        android:startOffset="@android:integer/config_shortAnimTime"
 | 
			
		||||
        android:valueFrom="-100.0"
 | 
			
		||||
        android:valueTo="0" />
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/decelerate_cubic"
 | 
			
		||||
        android:propertyName="elevation"
 | 
			
		||||
        android:startOffset="@android:integer/config_shortAnimTime"
 | 
			
		||||
        android:valueFrom="-100.0"
 | 
			
		||||
        android:valueTo="0" />
 | 
			
		||||
 | 
			
		||||
</set>
 | 
			
		||||
							
								
								
									
										27
									
								
								src/android/app/src/main/res/animator/setttings_pop_exit.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/android/app/src/main/res/animator/setttings_pop_exit.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/accelerate_cubic"
 | 
			
		||||
        android:propertyName="yFraction"
 | 
			
		||||
        android:valueFrom="0"
 | 
			
		||||
        android:valueTo="1.0" />
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/decelerate_cubic"
 | 
			
		||||
        android:propertyName="translationZ"
 | 
			
		||||
        android:startOffset="@android:integer/config_shortAnimTime"
 | 
			
		||||
        android:valueFrom="0.0"
 | 
			
		||||
        android:valueTo="100" />
 | 
			
		||||
 | 
			
		||||
    <objectAnimator
 | 
			
		||||
        android:duration="@android:integer/config_mediumAnimTime"
 | 
			
		||||
        android:interpolator="@android:interpolator/decelerate_cubic"
 | 
			
		||||
        android:propertyName="elevation"
 | 
			
		||||
        android:startOffset="@android:integer/config_shortAnimTime"
 | 
			
		||||
        android:valueFrom="0.0"
 | 
			
		||||
        android:valueTo="100" />
 | 
			
		||||
 | 
			
		||||
</set>
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/android/app/src/main/res/drawable-hdpi/button_a.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/android/app/src/main/res/drawable-hdpi/button_a.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 10 KiB  | 
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user