android: Add Citra frontend.
This commit is contained in:
parent
6549cf8bd0
commit
5fa0293c19
319 changed files with 13799 additions and 0 deletions
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue