diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 26d2fb869..60cce39af 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -117,8 +117,10 @@ endif() if (ENABLE_QT) add_subdirectory(citra_qt) endif() + if (ANDROID) - add_subdirectory(android/app/src/main/cpp) + include_directories(android/app/src/main) + add_subdirectory(android/app/src/main/jni) else() add_subdirectory(dedicated_room) endif() diff --git a/src/android/.gitignore b/src/android/.gitignore index 4423a0b45..40b6c5cd0 100644 --- a/src/android/.gitignore +++ b/src/android/.gitignore @@ -1,12 +1,46 @@ +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ *.iml -.gradle -/local.properties -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -.DS_Store -/build -/captures +.idea/ + +# Keystore files +# Uncomment the following line if you do not want to check your keystore files in. +#*.jks + +# External native build folder generated in Android Studio 2.2 and later .externalNativeBuild # CXX compile cache diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle index e57271517..012615ae2 100644 --- a/src/android/app/build.gradle +++ b/src/android/app/build.gradle @@ -1,8 +1,16 @@ apply plugin: 'com.android.application' +/** + * Use the number of seconds/10 since Jan 1 2016 as the versionCode. + * This lets us upload a new build at most every 10 seconds for the + * next 680 years. + */ +def autoVersion = (int) (((new Date().getTime() / 1000) - 1451606400) / 10) +def buildType +def abiFilter = "arm64-v8a" //, "x86" + android { - compileSdkVersion 26 - buildToolsVersion '28.0.3' + compileSdkVersion 29 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -13,34 +21,54 @@ android { // This is important as it will run lint but not abort on error // Lint has some overly obnoxious "errors" that should really be warnings abortOnError false + + //Uncomment disable lines for test builds... + //disable 'MissingTranslation'bin + //disable 'ExtraTranslation' } defaultConfig { - applicationId "org.citra_emu" - minSdkVersion 21 - targetSdkVersion 26 - - versionCode(getBuildVersionCode()) - - versionName "${getVersion()}" + // TODO If this is ever modified, change application_id in strings.xml + applicationId "org.citra.citra_emu" + minSdkVersion 26 + targetSdkVersion 29 + versionCode autoVersion + versionName getVersion() + ndk.abiFilters abiFilter } signingConfigs { - release { - if (project.hasProperty('keystore')) { - storeFile file(project.property('keystore')) - storePassword project.property('storepass') - keyAlias project.property('keyalias') - keyPassword project.property('keypass') - } - } + //release { + // storeFile file('') + // storePassword System.getenv('ANDROID_KEYPASS') + // keyAlias = 'key0' + // keyPassword System.getenv('ANDROID_KEYPASS') + //} + } + + applicationVariants.all { variant -> + buildType = variant.buildType.name // sets the current build type } // Define build types, which are orthogonal to product flavors. buildTypes { + // Signed by release key, allowing for upload to Play Store. release { - signingConfig signingConfigs.release + signingConfig signingConfigs.debug + } + + // builds a release build that doesn't need signing + // Attaches 'debug' suffix to version and package name, allowing installation alongside the release build. + relWithDebInfo { + initWith release + applicationIdSuffix ".debug" + versionNameSuffix '-debug' + signingConfig signingConfigs.debug + minifyEnabled false + testCoverageEnabled false + debuggable true + jniDebuggable true } // Signed by debug key disallowing distribution on Play Store. @@ -49,13 +77,14 @@ android { // TODO If this is ever modified, change application_id in debug/strings.xml applicationIdSuffix ".debug" versionNameSuffix '-debug' + debuggable true jniDebuggable true } } externalNativeBuild { cmake { - version getCmakeVersion() + version "3.10.2" path "../../../CMakeLists.txt" } } @@ -65,76 +94,46 @@ android { cmake { arguments "-DENABLE_QT=0", // Don't use QT "-DENABLE_SDL2=0", // Don't use SDL - "-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work - "-DENABLE_CUBEB=0", - "-DANDROID_STL=c++_shared" + "-DENABLE_WEB_SERVICE=0", // Don't use telemetry + "-DANDROID_ARM_NEON=true" // cryptopp requires Neon to work - abiFilters "arm64-v8a" - - targets "citra-android" + abiFilters abiFilter } } } } -ext { - androidSupportVersion = '26.1.0' -} - dependencies { - implementation "com.android.support:support-v13:$androidSupportVersion" - implementation "com.android.support:cardview-v7:$androidSupportVersion" - implementation "com.android.support:recyclerview-v7:$androidSupportVersion" - implementation "com.android.support:design:$androidSupportVersion" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.exifinterface:exifinterface:1.2.0' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'com.google.android.material:material:1.1.0' - // Android TV UI libraries. - implementation "com.android.support:leanback-v17:$androidSupportVersion" + // For loading huge screenshots from the disk. + implementation 'com.squareup.picasso:picasso:2.71828' - implementation 'com.android.support.constraint:constraint-layout:1.1.0' + // Allows FRP-style asynchronous operations in Android. + implementation 'io.reactivex:rxandroid:1.2.1' + implementation 'com.nononsenseapps:filepicker:4.2.1' + implementation 'org.ini4j:ini4j:0.5.4' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' - testImplementation "com.android.support.test:runner:1.0.2" - androidTestImplementation "com.android.support.test:runner:1.0.1" + implementation 'com.android.billingclient:billing:2.0.3' } def getVersion() { - def versionNumber = '0.0' + def versionName = '0.0' try { - versionNumber = 'git describe --always --long'.execute([], project.rootDir).text + versionName = 'git describe --always --long'.execute([], project.rootDir).text .trim() .replaceAll(/(-0)?-[^-]+$/, "") } catch (Exception e) { logger.error('Cannot find git, defaulting to dummy version number') } - return versionNumber -} - - -def getBuildVersionCode() { - try { - def versionNumber = 'git rev-list --first-parent --count HEAD'.execute([], project.rootDir).text - .trim() - return Integer.valueOf(versionNumber) - } catch (Exception e) { - logger.error('Cannot find git, defaulting to dummy version number') - } - - return 0 -} - -def getCmakeVersion() { - try { - // Tokenized form of the output will be - ["cmake", "version", "M.m.p-rcx"], the version number - // will be at index 2 - def version_string = 'cmake -version'.execute([], project.rootDir).text - .trim().tokenize()[2] - - return version_string - } - catch(Exception e) { - logger.error('Cannot find Cmake, using default Cmake') - } - - return null + return versionName } diff --git a/src/android/app/src/androidTest/java/org/citra_emu/citra/ExampleInstrumentedTest.java b/src/android/app/src/androidTest/java/org/citra/citra_emu/ExampleInstrumentedTest.java similarity index 84% rename from src/android/app/src/androidTest/java/org/citra_emu/citra/ExampleInstrumentedTest.java rename to src/android/app/src/androidTest/java/org/citra/citra_emu/ExampleInstrumentedTest.java index 7055de885..671fb4b30 100644 --- a/src/android/app/src/androidTest/java/org/citra_emu/citra/ExampleInstrumentedTest.java +++ b/src/android/app/src/androidTest/java/org/citra/citra_emu/ExampleInstrumentedTest.java @@ -1,4 +1,4 @@ -package org.citra_emu.citra; +package org.citra.citra_emu; import android.content.Context; import android.support.test.InstrumentationRegistry; @@ -21,6 +21,6 @@ public class ExampleInstrumentedTest { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); - assertEquals("org.citra_emu.citra_android", appContext.getPackageName()); + assertEquals("org.citra.citra_emu", appContext.getPackageName()); } } diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index c1e38446a..b51382d21 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -1,39 +1,91 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="org.citra_emu.citra"> + package="org.citra.citra_emu"> <uses-feature android:name="android.hardware.touchscreen" android:required="false"/> - <uses-feature android:name="android.hardware.gamepad" android:required="false"/> - <uses-feature android:glEsVersion="0x00030001" /> + <uses-feature android:glEsVersion="0x00030002" android:required="true" /> + + <uses-feature android:name="android.hardware.opengles.aep" android:required="true" /> + <uses-feature + android:name="android.hardware.camera.any" + android:required="false" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.RECORD_AUDIO" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <application - android:name="org.citra_emu.citra.CitraApplication" - android:label="Citra" - android:icon="@mipmap/ic_citra" - android:allowBackup="true" + android:name="org.citra.citra_emu.CitraApplication" + android:label="@string/app_name" + android:icon="@mipmap/ic_launcher" + android:allowBackup="false" android:supportsRtl="true" android:isGame="true" - android:banner="@mipmap/ic_citra"> + android:banner="@mipmap/ic_launcher" + android:requestLegacyExternalStorage="true"> <activity - android:name=".ui.main.MainActivity" - android:theme="@style/CitraBase"> + android:name="org.citra.citra_emu.ui.main.MainActivity" + android:theme="@style/CitraBase" + android:resizeableActivity="false"> <!-- This intentfilter marks this Activity as the one that gets launched from Home screen. --> <intent-filter> <action android:name="android.intent.action.MAIN"/> - <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + + <activity + android:name="org.citra.citra_emu.features.settings.ui.SettingsActivity" + android:configChanges="orientation|screenSize|uiMode" + android:theme="@style/CitraSettingsBase" + android:label="@string/preferences_settings"/> + + <activity + android:name="org.citra.citra_emu.activities.EmulationActivity" + android:resizeableActivity="false" + android:theme="@style/CitraEmulationBase" + android:launchMode="singleTop"/> + + <service android:name="org.citra.citra_emu.utils.ForegroundService"/> + + <activity + android:name="org.citra.citra_emu.activities.CustomFilePickerActivity" + android:label="@string/app_name" + android:theme="@style/FilePickerTheme"> + <intent-filter> + <action android:name="android.intent.action.GET_CONTENT" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + + <service android:name="org.citra.citra_emu.utils.DirectoryInitialization"/> + + <provider + android:name="org.citra.citra_emu.model.GameProvider" + android:authorities="${applicationId}.provider" + android:enabled="true" + android:exported="false"> + </provider> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.filesprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/nnf_provider_paths" /> + </provider> </application> </manifest> diff --git a/src/android/app/src/main/cpp/CMakeLists.txt b/src/android/app/src/main/cpp/CMakeLists.txt deleted file mode 100644 index f3a7e0131..000000000 --- a/src/android/app/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -cmake_minimum_required(VERSION 3.8) - -add_library(citra-android SHARED - logging/log.cpp - logging/logcat_backend.cpp - logging/logcat_backend.h - native_interface.cpp - native_interface.h - ui/main/main_activity.cpp - ) - -# find Android's log library -find_library(log-lib log) - -target_link_libraries(citra-android ${log-lib} core common inih) -target_include_directories(citra-android PRIVATE "../../../../../" "./") diff --git a/src/android/app/src/main/cpp/logging/log.cpp b/src/android/app/src/main/cpp/logging/log.cpp deleted file mode 100644 index 044f4eb4c..000000000 --- a/src/android/app/src/main/cpp/logging/log.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "common/logging/log.h" -#include "native_interface.h" - -namespace Log { -extern "C" { -JNICALL void Java_org_citra_1emu_citra_LOG_logEntry(JNIEnv* env, jclass type, jint level, - jstring file_name, jint line_number, - jstring function, jstring msg) { - using CitraJNI::GetJString; - FmtLogMessage(Class::Frontend, static_cast<Level>(level), GetJString(env, file_name).data(), - static_cast<unsigned int>(line_number), GetJString(env, function).data(), - GetJString(env, msg).data()); -} -} -} // namespace Log diff --git a/src/android/app/src/main/cpp/logging/logcat_backend.cpp b/src/android/app/src/main/cpp/logging/logcat_backend.cpp deleted file mode 100644 index 17b6ae1a0..000000000 --- a/src/android/app/src/main/cpp/logging/logcat_backend.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include <android/log.h> -#include "common/assert.h" -#include "common/logging/text_formatter.h" -#include "logcat_backend.h" - -namespace Log { -void LogcatBackend::Write(const Entry& entry) { - android_LogPriority priority; - switch (entry.log_level) { - case Level::Trace: - priority = ANDROID_LOG_VERBOSE; - break; - case Level::Debug: - priority = ANDROID_LOG_DEBUG; - break; - case Level::Info: - priority = ANDROID_LOG_INFO; - break; - case Level::Warning: - priority = ANDROID_LOG_WARN; - break; - case Level::Error: - priority = ANDROID_LOG_ERROR; - break; - case Level::Critical: - priority = ANDROID_LOG_FATAL; - break; - case Level::Count: - UNREACHABLE(); - } - - __android_log_print(priority, "citra", "%s\n", FormatLogMessage(entry).c_str()); -} -} // namespace Log \ No newline at end of file diff --git a/src/android/app/src/main/cpp/logging/logcat_backend.h b/src/android/app/src/main/cpp/logging/logcat_backend.h deleted file mode 100644 index f3bac4762..000000000 --- a/src/android/app/src/main/cpp/logging/logcat_backend.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include "common/logging/backend.h" - -namespace Log { -class LogcatBackend : public Backend { -public: - static const char* Name() { - return "Logcat"; - } - - const char* GetName() const override { - return Name(); - } - - void Write(const Entry& entry) override; -}; -} // namespace Log diff --git a/src/android/app/src/main/cpp/native_interface.cpp b/src/android/app/src/main/cpp/native_interface.cpp deleted file mode 100644 index fc4d73b77..000000000 --- a/src/android/app/src/main/cpp/native_interface.cpp +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include "native_interface.h" - -namespace CitraJNI { -jint JNI_OnLoad(JavaVM* vm, void* reserved) { - return JNI_VERSION_1_6; -} - -std::string GetJString(JNIEnv* env, jstring jstr) { - std::string result = ""; - if (!jstr) - return result; - - const char* s = env->GetStringUTFChars(jstr, nullptr); - result = s; - env->ReleaseStringUTFChars(jstr, s); - return result; -} -} // namespace CitraJNI diff --git a/src/android/app/src/main/cpp/native_interface.h b/src/android/app/src/main/cpp/native_interface.h deleted file mode 100644 index a7b99cb51..000000000 --- a/src/android/app/src/main/cpp/native_interface.h +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include <string> -#include <jni.h> - -namespace CitraJNI { -extern "C" { -jint JNI_OnLoad(JavaVM* vm, void* reserved); -} - -std::string GetJString(JNIEnv* env, jstring jstr); -} // namespace CitraJNI diff --git a/src/android/app/src/main/cpp/ui/main/main_activity.cpp b/src/android/app/src/main/cpp/ui/main/main_activity.cpp deleted file mode 100644 index b99ba1890..000000000 --- a/src/android/app/src/main/cpp/ui/main/main_activity.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include "common/common_paths.h" -#include "common/file_util.h" -#include "common/logging/filter.h" -#include "common/logging/log.h" -#include "core/settings.h" -#include "logging/logcat_backend.h" -#include "native_interface.h" - -namespace MainActivity { -extern "C" { -JNICALL void Java_org_citra_1emu_citra_ui_main_MainActivity_initUserPath(JNIEnv* env, jclass type, - jstring path) { - FileUtil::SetUserPath(CitraJNI::GetJString(env, path) + '/'); -} - -JNICALL void Java_org_citra_1emu_citra_ui_main_MainActivity_initLogging(JNIEnv* env, jclass type) { - Log::Filter log_filter(Log::Level::Debug); - log_filter.ParseFilterString(Settings::values.log_filter); - Log::SetGlobalFilter(log_filter); - - const std::string& log_dir = FileUtil::GetUserPath(FileUtil::UserPath::LogDir); - FileUtil::CreateFullPath(log_dir); - Log::AddBackend(std::make_unique<Log::FileBackend>(log_dir + LOG_FILE)); - Log::AddBackend(std::make_unique<Log::LogcatBackend>()); -} -}; -}; // namespace MainActivity diff --git a/src/android/app/src/main/ic_citra-web.png b/src/android/app/src/main/ic_citra-web.png deleted file mode 100644 index 129946a37..000000000 Binary files a/src/android/app/src/main/ic_citra-web.png and /dev/null differ diff --git a/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java b/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java new file mode 100644 index 000000000..9d3f80465 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/CitraApplication.java @@ -0,0 +1,55 @@ +// 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()); + } + + createNotificationChannel(); + + databaseHelper = new GameDatabase(this); + } + + public static Context getAppContext() { + return application.getApplicationContext(); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java new file mode 100644 index 000000000..48555252e --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.java @@ -0,0 +1,661 @@ +/* + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.citra.citra_emu; + +import android.app.Activity; +import android.app.Dialog; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.os.Bundle; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.Surface; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; + +import org.citra.citra_emu.activities.EmulationActivity; +import org.citra.citra_emu.applets.SoftwareKeyboard; +import org.citra.citra_emu.utils.EmulationMenuSettings; +import org.citra.citra_emu.utils.Log; +import org.citra.citra_emu.utils.PermissionsHandler; + +import java.lang.ref.WeakReference; +import java.util.Date; +import java.util.Objects; + +import static android.Manifest.permission.CAMERA; +import static android.Manifest.permission.RECORD_AUDIO; + +/** + * Class which contains methods that interact + * with the native side of the Citra code. + */ +public final class NativeLibrary { + /** + * Default touchscreen device + */ + public static final String TouchScreenDevice = "Touchscreen"; + public static WeakReference<EmulationActivity> sEmulationActivity = new WeakReference<>(null); + + private static boolean alertResult = false; + private static String alertPromptResult = ""; + private static int alertPromptButton = 0; + private static final Object alertPromptLock = new Object(); + private static boolean alertPromptInProgress = false; + private static String alertPromptCaption = ""; + private static int alertPromptButtonConfig = 0; + private static EditText alertPromptEditText = null; + + static { + try { + System.loadLibrary("main"); + } catch (UnsatisfiedLinkError ex) { + Log.error("[NativeLibrary] " + ex.toString()); + } + } + + private NativeLibrary() { + // Disallows instantiation. + } + + /** + * Handles button press events for a gamepad. + * + * @param Device The input descriptor of the gamepad. + * @param Button Key code identifying which button was pressed. + * @param Action Mask identifying which action is happening (button pressed down, or button released). + * @return If we handled the button press. + */ + public static native boolean onGamePadEvent(String Device, int Button, int Action); + + /** + * Handles gamepad movement events. + * + * @param Device The device ID of the gamepad. + * @param Axis The axis ID + * @param x_axis The value of the x-axis represented by the given ID. + * @param y_axis The value of the y-axis represented by the given ID + */ + public static native boolean onGamePadMoveEvent(String Device, int Axis, float x_axis, float y_axis); + + /** + * Handles gamepad movement events. + * + * @param Device The device ID of the gamepad. + * @param Axis_id The axis ID + * @param axis_val The value of the axis represented by the given ID. + */ + public static native boolean onGamePadAxisEvent(String Device, int Axis_id, float axis_val); + + /** + * Handles touch events. + * + * @param x_axis The value of the x-axis. + * @param y_axis The value of the y-axis + * @param pressed To identify if the touch held down or released. + * @return true if the pointer is within the touchscreen + */ + public static native boolean onTouchEvent(float x_axis, float y_axis, boolean pressed); + + /** + * Handles touch movement. + * + * @param x_axis The value of the instantaneous x-axis. + * @param y_axis The value of the instantaneous y-axis. + */ + public static native void onTouchMoved(float x_axis, float y_axis); + + public static native void ReloadSettings(); + + public static native String GetUserSetting(String gameID, String Section, String Key); + + public static native void SetUserSetting(String gameID, String Section, String Key, String Value); + + public static native void InitGameIni(String gameID); + + /** + * Gets the embedded icon within the given ROM. + * + * @param filename the file path to the ROM. + * @return an integer array containing the color data for the icon. + */ + public static native int[] GetIcon(String filename); + + /** + * Gets the embedded title of the given ISO/ROM. + * + * @param filename The file path to the ISO/ROM. + * @return the embedded title of the ISO/ROM. + */ + public static native String GetTitle(String filename); + + public static native String GetDescription(String filename); + + public static native String GetGameId(String filename); + + public static native String GetRegions(String filename); + + public static native String GetCompany(String filename); + + public static native String GetGitRevision(); + + /** + * Sets the current working user directory + * If not set, it auto-detects a location + */ + public static native void SetUserDirectory(String directory); + + public static native String[] GetInstalledGamePaths(); + + // Create the config.ini file. + public static native void CreateConfigFile(); + + public static native int DefaultCPUCore(); + + /** + * Begins emulation. + */ + public static native void Run(String path); + + public static native String[] GetTextureFilterNames(); + + /** + * Begins emulation from the specified savestate. + */ + public static native void Run(String path, String savestatePath, boolean deleteSavestate); + + // Surface Handling + public static native void SurfaceChanged(Surface surf); + + public static native void SurfaceDestroyed(); + + public static native void DoFrame(); + + /** + * Unpauses emulation from a paused state. + */ + public static native void UnPauseEmulation(); + + /** + * Pauses emulation. + */ + public static native void PauseEmulation(); + + /** + * Stops emulation. + */ + public static native void StopEmulation(); + + /** + * Returns true if emulation is running (or is paused). + */ + public static native boolean IsRunning(); + + /** + * Returns the performance stats for the current game + **/ + public static native double[] GetPerfStats(); + + /** + * Notifies the core emulation that the orientation has changed. + */ + public static native void NotifyOrientationChange(int layout_option, int rotation); + + /** + * Swaps the top and bottom screens. + */ + public static native void SwapScreens(boolean swap_screens, int rotation); + + public enum CoreError { + ErrorSystemFiles, + ErrorSavestate, + ErrorUnknown, + } + + private static boolean coreErrorAlertResult = false; + private static final Object coreErrorAlertLock = new Object(); + + public static class CoreErrorDialogFragment extends DialogFragment { + static CoreErrorDialogFragment newInstance(String title, String message) { + CoreErrorDialogFragment frag = new CoreErrorDialogFragment(); + Bundle args = new Bundle(); + args.putString("title", title); + args.putString("message", message); + frag.setArguments(args); + return frag; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity emulationActivity = Objects.requireNonNull(getActivity()); + + final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title")); + final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message")); + + return new AlertDialog.Builder(emulationActivity) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.continue_button, (dialog, which) -> { + coreErrorAlertResult = true; + synchronized (coreErrorAlertLock) { + coreErrorAlertLock.notify(); + } + }) + .setNegativeButton(R.string.abort_button, (dialog, which) -> { + coreErrorAlertResult = false; + synchronized (coreErrorAlertLock) { + coreErrorAlertLock.notify(); + } + }).setOnDismissListener(dialog -> { + coreErrorAlertResult = true; + synchronized (coreErrorAlertLock) { + coreErrorAlertLock.notify(); + } + }).create(); + } + } + + private static void OnCoreErrorImpl(String title, String message) { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.error("[NativeLibrary] EmulationActivity not present"); + return; + } + + CoreErrorDialogFragment fragment = CoreErrorDialogFragment.newInstance(title, message); + fragment.show(emulationActivity.getSupportFragmentManager(), "coreError"); + } + + /** + * Handles a core error. + * @return true: continue; false: abort + */ + public static boolean OnCoreError(CoreError error, String details) { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.error("[NativeLibrary] EmulationActivity not present"); + return false; + } + + String title, message; + switch (error) { + case ErrorSystemFiles: { + title = emulationActivity.getString(R.string.system_archive_not_found); + message = emulationActivity.getString(R.string.system_archive_not_found_message, details.isEmpty() ? emulationActivity.getString(R.string.system_archive_general) : details); + break; + } + case ErrorSavestate: { + title = emulationActivity.getString(R.string.save_load_error); + message = details; + break; + } + case ErrorUnknown: { + title = emulationActivity.getString(R.string.fatal_error); + message = emulationActivity.getString(R.string.fatal_error_message); + break; + } + default: { + return true; + } + } + + // Show the AlertDialog on the main thread. + emulationActivity.runOnUiThread(() -> OnCoreErrorImpl(title, message)); + + // Wait for the lock to notify that it is complete. + synchronized (coreErrorAlertLock) { + try { + coreErrorAlertLock.wait(); + } catch (Exception ignored) { + } + } + + return coreErrorAlertResult; + } + + public static boolean isPortraitMode() { + return CitraApplication.getAppContext().getResources().getConfiguration().orientation == + Configuration.ORIENTATION_PORTRAIT; + } + + public static int landscapeScreenLayout() { + return EmulationMenuSettings.getLandscapeScreenLayout(); + } + + public static boolean displayAlertMsg(final String caption, final String text, + final boolean yesNo) { + Log.error("[NativeLibrary] Alert: " + text); + final EmulationActivity emulationActivity = sEmulationActivity.get(); + boolean result = false; + if (emulationActivity == null) { + Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert."); + } else { + // Create object used for waiting. + final Object lock = new Object(); + AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) + .setTitle(caption) + .setMessage(text); + + // If not yes/no dialog just have one button that dismisses modal, + // otherwise have a yes and no button that sets alertResult accordingly. + if (!yesNo) { + builder + .setCancelable(false) + .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> + { + dialog.dismiss(); + synchronized (lock) { + lock.notify(); + } + }); + } else { + alertResult = false; + + builder + .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> + { + alertResult = true; + dialog.dismiss(); + synchronized (lock) { + lock.notify(); + } + }) + .setNegativeButton(android.R.string.no, (dialog, whichButton) -> + { + alertResult = false; + dialog.dismiss(); + synchronized (lock) { + lock.notify(); + } + }); + } + + // Show the AlertDialog on the main thread. + emulationActivity.runOnUiThread(builder::show); + + // Wait for the lock to notify that it is complete. + synchronized (lock) { + try { + lock.wait(); + } catch (Exception e) { + } + } + + if (yesNo) + result = alertResult; + } + return result; + } + + public static void retryDisplayAlertPrompt() { + if (!alertPromptInProgress) { + return; + } + displayAlertPromptImpl(alertPromptCaption, alertPromptEditText.getText().toString(), alertPromptButtonConfig).show(); + } + + public static String displayAlertPrompt(String caption, String text, int buttonConfig) { + alertPromptCaption = caption; + alertPromptButtonConfig = buttonConfig; + alertPromptInProgress = true; + + // Show the AlertDialog on the main thread + sEmulationActivity.get().runOnUiThread(() -> displayAlertPromptImpl(alertPromptCaption, text, alertPromptButtonConfig).show()); + + // Wait for the lock to notify that it is complete + synchronized (alertPromptLock) { + try { + alertPromptLock.wait(); + } catch (Exception e) { + } + } + alertPromptInProgress = false; + + return alertPromptResult; + } + + public static AlertDialog.Builder displayAlertPromptImpl(String caption, String text, int buttonConfig) { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + alertPromptResult = ""; + alertPromptButton = 0; + + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.leftMargin = params.rightMargin = CitraApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.dialog_margin); + + // Set up the input + alertPromptEditText = new EditText(CitraApplication.getAppContext()); + alertPromptEditText.setText(text); + alertPromptEditText.setSingleLine(); + alertPromptEditText.setLayoutParams(params); + + FrameLayout container = new FrameLayout(emulationActivity); + container.addView(alertPromptEditText); + + AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) + .setTitle(caption) + .setView(container) + .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> + { + alertPromptButton = buttonConfig; + alertPromptResult = alertPromptEditText.getText().toString(); + synchronized (alertPromptLock) { + alertPromptLock.notifyAll(); + } + }) + .setOnDismissListener(dialogInterface -> + { + alertPromptResult = ""; + synchronized (alertPromptLock) { + alertPromptLock.notifyAll(); + } + }); + + if (buttonConfig > 0) { + builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> + { + alertPromptResult = ""; + synchronized (alertPromptLock) { + alertPromptLock.notifyAll(); + } + }); + } + + return builder; + } + + public static int alertPromptButton() { + return alertPromptButton; + } + + public static void exitEmulationActivity(int resultCode) { + final int Success = 0; + final int ErrorNotInitialized = 1; + final int ErrorGetLoader = 2; + final int ErrorSystemMode = 3; + final int ErrorLoader = 4; + final int ErrorLoader_ErrorEncrypted = 5; + final int ErrorLoader_ErrorInvalidFormat = 6; + final int ErrorSystemFiles = 7; + final int ErrorVideoCore = 8; + final int ErrorVideoCore_ErrorGenericDrivers = 9; + final int ErrorVideoCore_ErrorBelowGL33 = 10; + final int ShutdownRequested = 11; + final int ErrorUnknown = 12; + + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.warning("[NativeLibrary] EmulationActivity is null, can't exit."); + return; + } + + int captionId = R.string.loader_error_invalid_format; + if (resultCode == ErrorLoader_ErrorEncrypted) { + captionId = R.string.loader_error_encrypted; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity) + .setTitle(captionId) + .setMessage(Html.fromHtml("Please follow the guides to redump your <a href=\"https://citra-emu.org/wiki/dumping-game-cartridges/\">game cartidges</a> or <a href=\"https://citra-emu.org/wiki/dumping-installed-titles/\">installed titles</a>.", Html.FROM_HTML_MODE_LEGACY)) + .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> emulationActivity.finish()) + .setOnDismissListener(dialogInterface -> emulationActivity.finish()); + emulationActivity.runOnUiThread(() -> { + AlertDialog alert = builder.create(); + alert.show(); + ((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); + }); + } + + public static void setEmulationActivity(EmulationActivity emulationActivity) { + Log.verbose("[NativeLibrary] Registering EmulationActivity."); + sEmulationActivity = new WeakReference<>(emulationActivity); + } + + public static void clearEmulationActivity() { + Log.verbose("[NativeLibrary] Unregistering EmulationActivity."); + + sEmulationActivity.clear(); + } + + private static final Object cameraPermissionLock = new Object(); + private static boolean cameraPermissionGranted = false; + public static final int REQUEST_CODE_NATIVE_CAMERA = 800; + + public static boolean RequestCameraPermission() { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.error("[NativeLibrary] EmulationActivity not present"); + return false; + } + if (ContextCompat.checkSelfPermission(emulationActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) { + // Permission already granted + return true; + } + emulationActivity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_NATIVE_CAMERA); + + // Wait until result is returned + synchronized (cameraPermissionLock) { + try { + cameraPermissionLock.wait(); + } catch (InterruptedException ignored) { + } + } + return cameraPermissionGranted; + } + + public static void CameraPermissionResult(boolean granted) { + cameraPermissionGranted = granted; + synchronized (cameraPermissionLock) { + cameraPermissionLock.notify(); + } + } + + private static final Object micPermissionLock = new Object(); + private static boolean micPermissionGranted = false; + public static final int REQUEST_CODE_NATIVE_MIC = 900; + + public static boolean RequestMicPermission() { + final EmulationActivity emulationActivity = sEmulationActivity.get(); + if (emulationActivity == null) { + Log.error("[NativeLibrary] EmulationActivity not present"); + return false; + } + if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + // Permission already granted + return true; + } + emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC); + + // Wait until result is returned + synchronized (micPermissionLock) { + try { + micPermissionLock.wait(); + } catch (InterruptedException ignored) { + } + } + return micPermissionGranted; + } + + public static void MicPermissionResult(boolean granted) { + micPermissionGranted = granted; + synchronized (micPermissionLock) { + micPermissionLock.notify(); + } + } + + /// Notifies that the activity is now in foreground and camera devices can now be reloaded + public static native void ReloadCameraDevices(); + + public static native boolean LoadAmiibo(byte[] bytes); + + public static native void RemoveAmiibo(); + + public static native void InstallCIAS(String[] path); + + public static final int SAVESTATE_SLOT_COUNT = 10; + + public static final class SavestateInfo { + public int slot; + public Date time; + } + + @Nullable + public static native SavestateInfo[] GetSavestateInfo(); + + public static native void SaveState(int slot); + public static native void LoadState(int slot); + + /** + * Button type for use in onTouchEvent + */ + public static final class ButtonType { + public static final int BUTTON_A = 700; + public static final int BUTTON_B = 701; + public static final int BUTTON_X = 702; + public static final int BUTTON_Y = 703; + public static final int BUTTON_START = 704; + public static final int BUTTON_SELECT = 705; + public static final int BUTTON_HOME = 706; + public static final int BUTTON_ZL = 707; + public static final int BUTTON_ZR = 708; + public static final int DPAD_UP = 709; + public static final int DPAD_DOWN = 710; + public static final int DPAD_LEFT = 711; + public static final int DPAD_RIGHT = 712; + public static final int STICK_LEFT = 713; + public static final int STICK_LEFT_UP = 714; + public static final int STICK_LEFT_DOWN = 715; + public static final int STICK_LEFT_LEFT = 716; + public static final int STICK_LEFT_RIGHT = 717; + public static final int STICK_C = 718; + public static final int STICK_C_UP = 719; + public static final int STICK_C_DOWN = 720; + public static final int STICK_C_LEFT = 771; + public static final int STICK_C_RIGHT = 772; + public static final int TRIGGER_L = 773; + public static final int TRIGGER_R = 774; + public static final int DPAD = 780; + public static final int BUTTON_DEBUG = 781; + public static final int BUTTON_GPIO14 = 782; + } + + /** + * Button states + */ + public static final class ButtonState { + public static final int RELEASED = 0; + public static final int PRESSED = 1; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java new file mode 100644 index 000000000..3083286e2 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/CustomFilePickerActivity.java @@ -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; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java new file mode 100644 index 000000000..adddcf110 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java @@ -0,0 +1,788 @@ +package org.citra.citra_emu.activities; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.util.SparseIntArray; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.SubMenu; +import android.view.View; +import android.widget.CheckBox; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.NotificationManagerCompat; +import androidx.fragment.app.FragmentActivity; + +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; +import org.citra.citra_emu.features.settings.ui.SettingsActivity; +import org.citra.citra_emu.features.settings.utils.SettingsFile; +import org.citra.citra_emu.camera.StillImageCameraHelper; +import org.citra.citra_emu.fragments.EmulationFragment; +import org.citra.citra_emu.ui.main.MainActivity; +import org.citra.citra_emu.utils.ControllerMappingHelper; +import org.citra.citra_emu.utils.EmulationMenuSettings; +import org.citra.citra_emu.utils.FileBrowserHelper; +import org.citra.citra_emu.utils.FileUtil; +import org.citra.citra_emu.utils.ForegroundService; + +import java.io.File; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.util.Collections; +import java.util.List; + +import static android.Manifest.permission.CAMERA; +import static android.Manifest.permission.RECORD_AUDIO; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +public final class EmulationActivity extends AppCompatActivity { + public static final String EXTRA_SELECTED_GAME = "SelectedGame"; + public static final String EXTRA_SELECTED_TITLE = "SelectedTitle"; + public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0; + public static final int MENU_ACTION_TOGGLE_CONTROLS = 1; + public static final int MENU_ACTION_ADJUST_SCALE = 2; + public static final int MENU_ACTION_EXIT = 3; + public static final int MENU_ACTION_SHOW_FPS = 4; + public static final int MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE = 5; + public static final int MENU_ACTION_SCREEN_LAYOUT_PORTRAIT = 6; + public static final int MENU_ACTION_SCREEN_LAYOUT_SINGLE = 7; + public static final int MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE = 8; + public static final int MENU_ACTION_SWAP_SCREENS = 9; + public static final int MENU_ACTION_RESET_OVERLAY = 10; + public static final int MENU_ACTION_SHOW_OVERLAY = 11; + public static final int MENU_ACTION_OPEN_SETTINGS = 12; + public static final int MENU_ACTION_LOAD_AMIIBO = 13; + public static final int MENU_ACTION_REMOVE_AMIIBO = 14; + public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15; + public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16; + + public static final int REQUEST_SELECT_AMIIBO = 2; + private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000; + private static SparseIntArray buttonsActionsMap = new SparseIntArray(); + + static { + buttonsActionsMap.append(R.id.menu_emulation_edit_layout, + EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT); + buttonsActionsMap.append(R.id.menu_emulation_toggle_controls, + EmulationActivity.MENU_ACTION_TOGGLE_CONTROLS); + buttonsActionsMap + .append(R.id.menu_emulation_adjust_scale, EmulationActivity.MENU_ACTION_ADJUST_SCALE); + buttonsActionsMap.append(R.id.menu_emulation_show_fps, + EmulationActivity.MENU_ACTION_SHOW_FPS); + buttonsActionsMap.append(R.id.menu_screen_layout_landscape, + EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE); + buttonsActionsMap.append(R.id.menu_screen_layout_portrait, + EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_PORTRAIT); + buttonsActionsMap.append(R.id.menu_screen_layout_single, + EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SINGLE); + buttonsActionsMap.append(R.id.menu_screen_layout_sidebyside, + EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE); + buttonsActionsMap.append(R.id.menu_emulation_swap_screens, + EmulationActivity.MENU_ACTION_SWAP_SCREENS); + buttonsActionsMap + .append(R.id.menu_emulation_reset_overlay, EmulationActivity.MENU_ACTION_RESET_OVERLAY); + buttonsActionsMap + .append(R.id.menu_emulation_show_overlay, EmulationActivity.MENU_ACTION_SHOW_OVERLAY); + buttonsActionsMap + .append(R.id.menu_emulation_open_settings, EmulationActivity.MENU_ACTION_OPEN_SETTINGS); + buttonsActionsMap + .append(R.id.menu_emulation_amiibo_load, EmulationActivity.MENU_ACTION_LOAD_AMIIBO); + buttonsActionsMap + .append(R.id.menu_emulation_amiibo_remove, EmulationActivity.MENU_ACTION_REMOVE_AMIIBO); + buttonsActionsMap.append(R.id.menu_emulation_joystick_rel_center, + EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER); + buttonsActionsMap.append(R.id.menu_emulation_dpad_slide_enable, + EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE); + } + + private View mDecorView; + private EmulationFragment mEmulationFragment; + private SharedPreferences mPreferences; + private ControllerMappingHelper mControllerMappingHelper; + private Intent foregroundService; + private boolean activityRecreated; + private String mSelectedTitle; + private String mPath; + + public static void launch(FragmentActivity activity, String path, String title) { + Intent launcher = new Intent(activity, EmulationActivity.class); + + launcher.putExtra(EXTRA_SELECTED_GAME, path); + launcher.putExtra(EXTRA_SELECTED_TITLE, title); + activity.startActivity(launcher); + } + + public static void tryDismissRunningNotification(Activity activity) { + NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION); + } + + @Override + protected void onDestroy() { + stopService(foregroundService); + super.onDestroy(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + // Get params we were passed + Intent gameToEmulate = getIntent(); + mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME); + mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE); + activityRecreated = false; + } else { + activityRecreated = true; + restoreState(savedInstanceState); + } + + mControllerMappingHelper = new ControllerMappingHelper(); + + // Get a handle to the Window containing the UI. + mDecorView = getWindow().getDecorView(); + mDecorView.setOnSystemUiVisibilityChangeListener(visibility -> + { + if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { + // Go back to immersive fullscreen mode in 3s + Handler handler = new Handler(getMainLooper()); + handler.postDelayed(this::enableFullscreenImmersive, 3000 /* 3s */); + } + }); + // Set these options now so that the SurfaceView the game renders into is the right size. + enableFullscreenImmersive(); + + setTheme(R.style.CitraEmulationBase); + + setContentView(R.layout.activity_emulation); + + // Find or create the EmulationFragment + mEmulationFragment = (EmulationFragment) getSupportFragmentManager() + .findFragmentById(R.id.frame_emulation_fragment); + if (mEmulationFragment == null) { + mEmulationFragment = EmulationFragment.newInstance(mPath); + getSupportFragmentManager().beginTransaction() + .add(R.id.frame_emulation_fragment, mEmulationFragment) + .commit(); + } + + setTitle(mSelectedTitle); + + mPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Start a foreground service to prevent the app from getting killed in the background + foregroundService = new Intent(EmulationActivity.this, ForegroundService.class); + startForegroundService(foregroundService); + + // Override Citra core INI with the one set by our in game menu + NativeLibrary.SwapScreens(EmulationMenuSettings.getSwapScreens(), + getWindowManager().getDefaultDisplay().getRotation()); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + outState.putString(EXTRA_SELECTED_GAME, mPath); + outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle); + super.onSaveInstanceState(outState); + } + + protected void restoreState(Bundle savedInstanceState) { + mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME); + mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE); + + // If an alert prompt was in progress when state was restored, retry displaying it + NativeLibrary.retryDisplayAlertPrompt(); + } + + @Override + public void onRestart() { + super.onRestart(); + NativeLibrary.ReloadCameraDevices(); + } + + @Override + public void onBackPressed() { + NativeLibrary.PauseEmulation(); + new AlertDialog.Builder(this) + .setTitle(R.string.emulation_close_game) + .setMessage(R.string.emulation_close_game_message) + .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> + { + mEmulationFragment.stopEmulation(); + finish(); + }) + .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> + NativeLibrary.UnPauseEmulation()) + .setOnCancelListener(dialogInterface -> + NativeLibrary.UnPauseEmulation()) + .create() + .show(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA: + if (grantResults[0] != PackageManager.PERMISSION_GRANTED && + shouldShowRequestPermissionRationale(CAMERA)) { + new AlertDialog.Builder(this) + .setTitle(R.string.camera) + .setMessage(R.string.camera_permission_needed) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED); + break; + case NativeLibrary.REQUEST_CODE_NATIVE_MIC: + if (grantResults[0] != PackageManager.PERMISSION_GRANTED && + shouldShowRequestPermissionRationale(RECORD_AUDIO)) { + new AlertDialog.Builder(this) + .setTitle(R.string.microphone) + .setMessage(R.string.microphone_permission_needed) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + NativeLibrary.MicPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED); + break; + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + break; + } + } + + private void enableFullscreenImmersive() { + // It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar. + mDecorView.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_emulation, menu); + + int layoutOptionMenuItem = R.id.menu_screen_layout_landscape; + switch (EmulationMenuSettings.getLandscapeScreenLayout()) { + case EmulationMenuSettings.LayoutOption_SingleScreen: + layoutOptionMenuItem = R.id.menu_screen_layout_single; + break; + case EmulationMenuSettings.LayoutOption_SideScreen: + layoutOptionMenuItem = R.id.menu_screen_layout_sidebyside; + break; + case EmulationMenuSettings.LayoutOption_MobilePortrait: + layoutOptionMenuItem = R.id.menu_screen_layout_portrait; + break; + } + + menu.findItem(layoutOptionMenuItem).setChecked(true); + menu.findItem(R.id.menu_emulation_joystick_rel_center).setChecked(EmulationMenuSettings.getJoystickRelCenter()); + menu.findItem(R.id.menu_emulation_dpad_slide_enable).setChecked(EmulationMenuSettings.getDpadSlideEnable()); + menu.findItem(R.id.menu_emulation_show_fps).setChecked(EmulationMenuSettings.getShowFps()); + menu.findItem(R.id.menu_emulation_swap_screens).setChecked(EmulationMenuSettings.getSwapScreens()); + menu.findItem(R.id.menu_emulation_show_overlay).setChecked(EmulationMenuSettings.getShowOverlay()); + + return true; + } + + private void DisplaySavestateWarning() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); + if (preferences.getBoolean("savestateWarningShown", false)) { + return; + } + + LayoutInflater inflater = mEmulationFragment.requireActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.dialog_checkbox, null); + CheckBox checkBox = view.findViewById(R.id.checkBox); + + new AlertDialog.Builder(this) + .setTitle(R.string.savestate_warning_title) + .setMessage(R.string.savestate_warning_message) + .setView(view) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + preferences.edit().putBoolean("savestateWarningShown", checkBox.isChecked()).apply(); + }) + .show(); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + final NativeLibrary.SavestateInfo[] savestates = NativeLibrary.GetSavestateInfo(); + if (savestates == null) { + menu.findItem(R.id.menu_emulation_save_state).setVisible(false); + menu.findItem(R.id.menu_emulation_load_state).setVisible(false); + return true; + } + menu.findItem(R.id.menu_emulation_save_state).setVisible(true); + menu.findItem(R.id.menu_emulation_load_state).setVisible(true); + + final SubMenu saveStateMenu = menu.findItem(R.id.menu_emulation_save_state).getSubMenu(); + final SubMenu loadStateMenu = menu.findItem(R.id.menu_emulation_load_state).getSubMenu(); + saveStateMenu.clear(); + loadStateMenu.clear(); + + // Update savestates information + for (int i = 0; i < NativeLibrary.SAVESTATE_SLOT_COUNT; ++i) { + final int slot = i + 1; + final String text = getString(R.string.emulation_empty_state_slot, slot); + saveStateMenu.add(text).setEnabled(true).setOnMenuItemClickListener((item) -> { + DisplaySavestateWarning(); + NativeLibrary.SaveState(slot); + return true; + }); + loadStateMenu.add(text).setEnabled(false).setOnMenuItemClickListener((item) -> { + NativeLibrary.LoadState(slot); + return true; + }); + } + for (final NativeLibrary.SavestateInfo info : savestates) { + final String text = getString(R.string.emulation_occupied_state_slot, info.slot, info.time); + saveStateMenu.getItem(info.slot - 1).setTitle(text); + loadStateMenu.getItem(info.slot - 1).setTitle(text).setEnabled(true); + } + return true; + } + + @SuppressWarnings("WrongConstant") + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int action = buttonsActionsMap.get(item.getItemId(), -1); + + switch (action) { + // Edit the placement of the controls + case MENU_ACTION_EDIT_CONTROLS_PLACEMENT: + editControlsPlacement(); + break; + + // Enable/Disable specific buttons or the entire input overlay. + case MENU_ACTION_TOGGLE_CONTROLS: + toggleControls(); + break; + + // Adjust the scale of the overlay controls. + case MENU_ACTION_ADJUST_SCALE: + adjustScale(); + break; + + // Toggle the visibility of the Performance stats TextView + case MENU_ACTION_SHOW_FPS: { + final boolean isEnabled = !EmulationMenuSettings.getShowFps(); + EmulationMenuSettings.setShowFps(isEnabled); + item.setChecked(isEnabled); + + mEmulationFragment.updateShowFpsOverlay(); + break; + } + // Sets the screen layout to Landscape + case MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE: + changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, item); + break; + + // Sets the screen layout to Portrait + case MENU_ACTION_SCREEN_LAYOUT_PORTRAIT: + changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, item); + break; + + // Sets the screen layout to Single + case MENU_ACTION_SCREEN_LAYOUT_SINGLE: + changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, item); + break; + + // Sets the screen layout to Side by Side + case MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE: + changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, item); + break; + + // Swap the top and bottom screen locations + case MENU_ACTION_SWAP_SCREENS: { + final boolean isEnabled = !EmulationMenuSettings.getSwapScreens(); + EmulationMenuSettings.setSwapScreens(isEnabled); + item.setChecked(isEnabled); + + NativeLibrary.SwapScreens(isEnabled, getWindowManager().getDefaultDisplay() + .getRotation()); + break; + } + + // Reset overlay placement + case MENU_ACTION_RESET_OVERLAY: + resetOverlay(); + break; + + // Show or hide overlay + case MENU_ACTION_SHOW_OVERLAY: { + final boolean isEnabled = !EmulationMenuSettings.getShowOverlay(); + EmulationMenuSettings.setShowOverlay(isEnabled); + item.setChecked(isEnabled); + + mEmulationFragment.refreshInputOverlay(); + break; + } + + case MENU_ACTION_EXIT: + mEmulationFragment.stopEmulation(); + finish(); + break; + + case MENU_ACTION_OPEN_SETTINGS: + SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, ""); + break; + + case MENU_ACTION_LOAD_AMIIBO: + FileBrowserHelper.openFilePicker(this, REQUEST_SELECT_AMIIBO, + R.string.select_amiibo, + Collections.singletonList("bin"), false); + break; + + case MENU_ACTION_REMOVE_AMIIBO: + RemoveAmiibo(); + break; + + case MENU_ACTION_JOYSTICK_REL_CENTER: + final boolean isJoystickRelCenterEnabled = !EmulationMenuSettings.getJoystickRelCenter(); + EmulationMenuSettings.setJoystickRelCenter(isJoystickRelCenterEnabled); + item.setChecked(isJoystickRelCenterEnabled); + break; + case MENU_ACTION_DPAD_SLIDE_ENABLE: + final boolean isDpadSlideEnabled = !EmulationMenuSettings.getDpadSlideEnable(); + EmulationMenuSettings.setDpadSlideEnable(isDpadSlideEnabled); + item.setChecked(isDpadSlideEnabled); + break; + } + + return true; + } + + private void changeScreenOrientation(int layoutOption, MenuItem item) { + item.setChecked(true); + NativeLibrary.NotifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay() + .getRotation()); + EmulationMenuSettings.setLandscapeScreenLayout(layoutOption); + } + + private void editControlsPlacement() { + if (mEmulationFragment.isConfiguringControls()) { + mEmulationFragment.stopConfiguringControls(); + } else { + mEmulationFragment.startConfiguringControls(); + } + } + + // Gets button presses + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + int action; + int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode()); + + switch (event.getAction()) { + case KeyEvent.ACTION_DOWN: + // Handling the case where the back button is pressed. + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + onBackPressed(); + return true; + } + + // Normal key events. + action = NativeLibrary.ButtonState.PRESSED; + break; + case KeyEvent.ACTION_UP: + action = NativeLibrary.ButtonState.RELEASED; + break; + default: + return false; + } + InputDevice input = event.getDevice(); + + if (input == null) { + // Controller was disconnected + return false; + } + + return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent result) { + super.onActivityResult(requestCode, resultCode, result); + switch (requestCode) { + case StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER: + StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null); + break; + case REQUEST_SELECT_AMIIBO: + // If the user picked a file, as opposed to just backing out. + if (resultCode == MainActivity.RESULT_OK) { + String[] selectedFiles = FileBrowserHelper.getSelectedFiles(result); + if (selectedFiles == null) + return; + + onAmiiboSelected(selectedFiles[0]); + } + break; + } + } + + private void onAmiiboSelected(String selectedFile) { + File file = new File(selectedFile); + boolean success = false; + try { + byte[] bytes = FileUtil.getBytesFromFile(file); + success = NativeLibrary.LoadAmiibo(bytes); + } catch (IOException e) { + e.printStackTrace(); + } + + if (!success) { + new AlertDialog.Builder(this) + .setTitle(R.string.amiibo_load_error) + .setMessage(R.string.amiibo_load_error_message) + .setPositiveButton(android.R.string.ok, null) + .create() + .show(); + } + } + + private void RemoveAmiibo() { + NativeLibrary.RemoveAmiibo(); + } + + private void toggleControls() { + final SharedPreferences.Editor editor = mPreferences.edit(); + boolean[] enabledButtons = new boolean[14]; + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.emulation_toggle_controls); + + for (int i = 0; i < enabledButtons.length; i++) { + // Buttons that are disabled by default + boolean defaultValue = true; + switch (i) { + case 6: // ZL + case 7: // ZR + case 12: // C-stick + defaultValue = false; + break; + } + + enabledButtons[i] = mPreferences.getBoolean("buttonToggle" + i, defaultValue); + } + builder.setMultiChoiceItems(R.array.n3dsButtons, enabledButtons, + (dialog, indexSelected, isChecked) -> editor + .putBoolean("buttonToggle" + indexSelected, isChecked)); + builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> + { + editor.apply(); + + mEmulationFragment.refreshInputOverlay(); + }); + + AlertDialog alertDialog = builder.create(); + alertDialog.show(); + } + + private void adjustScale() { + LayoutInflater inflater = LayoutInflater.from(this); + View view = inflater.inflate(R.layout.dialog_seekbar, null); + + final SeekBar seekbar = view.findViewById(R.id.seekbar); + final TextView value = view.findViewById(R.id.text_value); + final TextView units = view.findViewById(R.id.text_units); + + seekbar.setMax(150); + seekbar.setProgress(mPreferences.getInt("controlScale", 50)); + seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + public void onStartTrackingTouch(SeekBar seekBar) { + } + + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + value.setText(String.valueOf(progress + 50)); + } + + public void onStopTrackingTouch(SeekBar seekBar) { + setControlScale(seekbar.getProgress()); + } + }); + + value.setText(String.valueOf(seekbar.getProgress() + 50)); + units.setText("%"); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.emulation_control_scale); + builder.setView(view); + final int previousProgress = seekbar.getProgress(); + builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> { + setControlScale(previousProgress); + }); + builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> + { + setControlScale(seekbar.getProgress()); + }); + builder.setNeutralButton(R.string.slider_default, (dialogInterface, i) -> { + setControlScale(50); + }); + + AlertDialog alertDialog = builder.create(); + alertDialog.show(); + } + + private void setControlScale(int scale) { + SharedPreferences.Editor editor = mPreferences.edit(); + editor.putInt("controlScale", scale); + editor.apply(); + mEmulationFragment.refreshInputOverlay(); + } + + private void resetOverlay() { + new AlertDialog.Builder(this) + .setTitle(getString(R.string.emulation_touch_overlay_reset)) + .setPositiveButton(android.R.string.yes, (dialogInterface, i) -> mEmulationFragment.resetInputOverlay()) + .setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> { + }) + .create() + .show(); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)) { + return super.dispatchGenericMotionEvent(event); + } + + // Don't attempt to do anything if we are disconnecting a device. + if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) { + return true; + } + + InputDevice input = event.getDevice(); + List<InputDevice.MotionRange> motions = input.getMotionRanges(); + + float[] axisValuesCirclePad = {0.0f, 0.0f}; + float[] axisValuesCStick = {0.0f, 0.0f}; + float[] axisValuesDPad = {0.0f, 0.0f}; + boolean isTriggerPressedLMapped = false; + boolean isTriggerPressedRMapped = false; + boolean isTriggerPressedZLMapped = false; + boolean isTriggerPressedZRMapped = false; + boolean isTriggerPressedL = false; + boolean isTriggerPressedR = false; + boolean isTriggerPressedZL = false; + boolean isTriggerPressedZR = false; + + for (InputDevice.MotionRange range : motions) { + int axis = range.getAxis(); + float origValue = event.getAxisValue(axis); + float value = mControllerMappingHelper.scaleAxis(input, axis, origValue); + int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1); + int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1); + + if (nextMapping == -1 || guestOrientation == -1) { + // Axis is unmapped + continue; + } + + if ((value > 0.f && value < 0.1f) || (value < 0.f && value > -0.1f)) { + // Skip joystick wobble + value = 0.f; + } + + if (nextMapping == NativeLibrary.ButtonType.STICK_LEFT) { + axisValuesCirclePad[guestOrientation] = value; + } else if (nextMapping == NativeLibrary.ButtonType.STICK_C) { + axisValuesCStick[guestOrientation] = value; + } else if (nextMapping == NativeLibrary.ButtonType.DPAD) { + axisValuesDPad[guestOrientation] = value; + } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_L) { + isTriggerPressedLMapped = true; + isTriggerPressedL = value != 0.f; + } else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_R) { + isTriggerPressedRMapped = true; + isTriggerPressedR = value != 0.f; + } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZL) { + isTriggerPressedZLMapped = true; + isTriggerPressedZL = value != 0.f; + } else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZR) { + isTriggerPressedZRMapped = true; + isTriggerPressedZR = value != 0.f; + } + } + + // Circle-Pad and C-Stick status + NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]); + NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]); + + // Triggers L/R and ZL/ZR + if (isTriggerPressedLMapped) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); + } + if (isTriggerPressedRMapped) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); + } + if (isTriggerPressedZLMapped) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); + } + if (isTriggerPressedZRMapped) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED); + } + + // Work-around to allow D-pad axis to be bound to emulated buttons + if (axisValuesDPad[0] == 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED); + } + if (axisValuesDPad[0] < 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED); + } + if (axisValuesDPad[0] > 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED); + } + if (axisValuesDPad[1] == 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED); + } + if (axisValuesDPad[1] < 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED); + } + if (axisValuesDPad[1] > 0.f) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED); + } + + return true; + } + + public boolean isActivityRecreated() { + return activityRecreated; + } + + @Retention(SOURCE) + @IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE, + MENU_ACTION_EXIT, MENU_ACTION_SHOW_FPS, MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE, + MENU_ACTION_SCREEN_LAYOUT_PORTRAIT, MENU_ACTION_SCREEN_LAYOUT_SINGLE, MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE, + MENU_ACTION_SWAP_SCREENS, MENU_ACTION_RESET_OVERLAY, MENU_ACTION_SHOW_OVERLAY, MENU_ACTION_OPEN_SETTINGS}) + public @interface MenuAction { + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java new file mode 100644 index 000000000..bc791638a --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.java @@ -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(); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java b/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java new file mode 100644 index 000000000..85b55b00d --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java @@ -0,0 +1,124 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.applets; + +import android.app.Activity; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; + +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.R; +import org.citra.citra_emu.activities.EmulationActivity; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Objects; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + +public final class MiiSelector { + public static class MiiSelectorConfig implements java.io.Serializable { + public boolean enable_cancel_button; + public String title; + public long initially_selected_mii_index; + // List of Miis to display + public String[] mii_names; + } + + public static class MiiSelectorData { + public long return_code; + public int index; + + private MiiSelectorData(long return_code, int index) { + this.return_code = return_code; + this.index = index; + } + } + + public static class MiiSelectorDialogFragment extends DialogFragment { + static MiiSelectorDialogFragment newInstance(MiiSelectorConfig config) { + MiiSelectorDialogFragment frag = new MiiSelectorDialogFragment(); + Bundle args = new Bundle(); + args.putSerializable("config", config); + frag.setArguments(args); + return frag; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity emulationActivity = Objects.requireNonNull(getActivity()); + + MiiSelectorConfig config = + Objects.requireNonNull((MiiSelectorConfig) Objects.requireNonNull(getArguments()) + .getSerializable("config")); + + // Note: we intentionally leave out the Standard Mii in the native code so that + // the string can get translated + ArrayList<String> list = new ArrayList<>(); + list.add(emulationActivity.getString(R.string.standard_mii)); + list.addAll(Arrays.asList(config.mii_names)); + + final int initialIndex = config.initially_selected_mii_index < list.size() + ? (int) config.initially_selected_mii_index + : 0; + data.index = initialIndex; + AlertDialog.Builder builder = + new AlertDialog.Builder(emulationActivity) + .setTitle(config.title.isEmpty() + ? emulationActivity.getString(R.string.mii_selector) + : config.title) + .setSingleChoiceItems(list.toArray(new String[]{}), initialIndex, + (dialog, which) -> { + data.index = which; + }) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + data.return_code = 0; + synchronized (finishLock) { + finishLock.notifyAll(); + } + }); + if (config.enable_cancel_button) { + builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> { + data.return_code = 1; + synchronized (finishLock) { + finishLock.notifyAll(); + } + }); + } + setCancelable(false); + return builder.create(); + } + } + + private static MiiSelectorData data; + private static final Object finishLock = new Object(); + + private static void ExecuteImpl(MiiSelectorConfig config) { + final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); + + data = new MiiSelectorData(0, 0); + + MiiSelectorDialogFragment fragment = MiiSelectorDialogFragment.newInstance(config); + fragment.show(emulationActivity.getSupportFragmentManager(), "mii_selector"); + } + + public static MiiSelectorData Execute(MiiSelectorConfig config) { + NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config)); + + synchronized (finishLock) { + try { + finishLock.wait(); + } catch (Exception ignored) { + } + } + + return data; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java b/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java new file mode 100644 index 000000000..7be5f6d97 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java @@ -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); +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java new file mode 100644 index 000000000..701cb0710 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/camera/StillImageCameraHelper.java @@ -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); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java b/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java new file mode 100644 index 000000000..0f10f1858 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java @@ -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; + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java b/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java new file mode 100644 index 000000000..8f9d215a3 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress.java @@ -0,0 +1,139 @@ +// Copyright 2021 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.disk_shader_cache; + +import android.app.Activity; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.R; +import org.citra.citra_emu.activities.EmulationActivity; +import org.citra.citra_emu.utils.Log; + +import java.util.Objects; + +public class DiskShaderCacheProgress { + + // Equivalent to VideoCore::LoadCallbackStage + public enum LoadCallbackStage { + Prepare, + Decompile, + Build, + Complete, + } + + private static final Object finishLock = new Object(); + private static ProgressDialogFragment fragment; + + public static class ProgressDialogFragment extends DialogFragment { + ProgressBar progressBar; + TextView progressText; + AlertDialog dialog; + + static ProgressDialogFragment newInstance(String title, String message) { + ProgressDialogFragment frag = new ProgressDialogFragment(); + Bundle args = new Bundle(); + args.putString("title", title); + args.putString("message", message); + frag.setArguments(args); + return frag; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Activity emulationActivity = Objects.requireNonNull(getActivity()); + + final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title")); + final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message")); + + LayoutInflater inflater = LayoutInflater.from(emulationActivity); + View view = inflater.inflate(R.layout.dialog_progress_bar, null); + + progressBar = view.findViewById(R.id.progress_bar); + progressText = view.findViewById(R.id.progress_text); + progressText.setText(""); + + setCancelable(false); + setRetainInstance(true); + + AlertDialog.Builder builder = new AlertDialog.Builder(emulationActivity); + builder.setTitle(title); + builder.setMessage(message); + builder.setView(view); + builder.setNegativeButton(android.R.string.cancel, null); + + dialog = builder.create(); + dialog.create(); + + dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v) -> emulationActivity.onBackPressed()); + + synchronized (finishLock) { + finishLock.notifyAll(); + } + + return dialog; + } + + private void onUpdateProgress(String msg, int progress, int max) { + Objects.requireNonNull(getActivity()).runOnUiThread(() -> { + progressBar.setProgress(progress); + progressBar.setMax(max); + progressText.setText(String.format("%d/%d", progress, max)); + dialog.setMessage(msg); + }); + } + } + + private static void prepareDialog() { + NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> { + final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); + fragment = ProgressDialogFragment.newInstance(emulationActivity.getString(R.string.loading), emulationActivity.getString(R.string.preparing_shaders)); + fragment.show(emulationActivity.getSupportFragmentManager(), "diskShaders"); + }); + + synchronized (finishLock) { + try { + finishLock.wait(); + } catch (Exception ignored) { + } + } + } + + public static void loadProgress(LoadCallbackStage stage, int progress, int max) { + final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get(); + if (emulationActivity == null) { + Log.error("[DiskShaderCacheProgress] EmulationActivity not present"); + return; + } + + switch (stage) { + case Prepare: + prepareDialog(); + break; + case Decompile: + fragment.onUpdateProgress(emulationActivity.getString(R.string.preparing_shaders), progress, max); + break; + case Build: + fragment.onUpdateProgress(emulationActivity.getString(R.string.building_shaders), progress, max); + break; + case Complete: + // Workaround for when dialog is dismissed when the app is in the background + fragment.dismissAllowingStateLoss(); + break; + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java new file mode 100644 index 000000000..932dcf1d3 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java @@ -0,0 +1,23 @@ +package org.citra.citra_emu.features.settings.model; + +public final class BooleanSetting extends Setting { + private boolean mValue; + + public BooleanSetting(String key, String section, boolean value) { + super(key, section); + mValue = value; + } + + public boolean getValue() { + return mValue; + } + + public void setValue(boolean value) { + mValue = value; + } + + @Override + public String getValueAsString() { + return mValue ? "True" : "False"; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java new file mode 100644 index 000000000..275f0ecea --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java @@ -0,0 +1,23 @@ +package org.citra.citra_emu.features.settings.model; + +public final class FloatSetting extends Setting { + private float mValue; + + public FloatSetting(String key, String section, float value) { + super(key, section); + mValue = value; + } + + public float getValue() { + return mValue; + } + + public void setValue(float value) { + mValue = value; + } + + @Override + public String getValueAsString() { + return Float.toString(mValue); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java new file mode 100644 index 000000000..f712e5bfa --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java @@ -0,0 +1,23 @@ +package org.citra.citra_emu.features.settings.model; + +public final class IntSetting extends Setting { + private int mValue; + + public IntSetting(String key, String section, int value) { + super(key, section); + mValue = value; + } + + public int getValue() { + return mValue; + } + + public void setValue(int value) { + mValue = value; + } + + @Override + public String getValueAsString() { + return Integer.toString(mValue); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java new file mode 100644 index 000000000..b762847c9 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java @@ -0,0 +1,42 @@ +package org.citra.citra_emu.features.settings.model; + +/** + * Abstraction for a setting item as read from / written to Citra's configuration ini files. + * These files generally consist of a key/value pair, though the type of value is ambiguous and + * must be inferred at read-time. The type of value determines which child of this class is used + * to represent the Setting. + */ +public abstract class Setting { + private String mKey; + private String mSection; + + /** + * Base constructor. + * + * @param key Everything to the left of the = in a line from the ini file. + * @param section The corresponding recent section header; e.g. [Core] or [Enhancements] without the brackets. + */ + public Setting(String key, String section) { + mKey = key; + mSection = section; + } + + /** + * @return The identifier used to write this setting to the ini file. + */ + public String getKey() { + return mKey; + } + + /** + * @return The name of the header under which this Setting should be written in the ini file. + */ + public String getSection() { + return mSection; + } + + /** + * @return A representation of this Setting's backing value converted to a String (e.g. for serialization). + */ + public abstract String getValueAsString(); +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java new file mode 100644 index 000000000..0a291aa6b --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java @@ -0,0 +1,55 @@ +package org.citra.citra_emu.features.settings.model; + +import java.util.HashMap; + +/** + * A semantically-related group of Settings objects. These Settings are + * internally stored as a HashMap. + */ +public final class SettingSection { + private String mName; + + private HashMap<String, Setting> mSettings = new HashMap<>(); + + /** + * Create a new SettingSection with no Settings in it. + * + * @param name The header of this section; e.g. [Core] or [Enhancements] without the brackets. + */ + public SettingSection(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + /** + * Convenience method; inserts a value directly into the backing HashMap. + * + * @param setting The Setting to be inserted. + */ + public void putSetting(Setting setting) { + mSettings.put(setting.getKey(), setting); + } + + /** + * Convenience method; gets a value directly from the backing HashMap. + * + * @param key Used to retrieve the Setting. + * @return A Setting object (you should probably cast this before using) + */ + public Setting getSetting(String key) { + return mSettings.get(key); + } + + public HashMap<String, Setting> getSettings() { + return mSettings; + } + + public void mergeSection(SettingSection settingSection) { + for (Setting setting : settingSection.mSettings.values()) { + putSetting(setting); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java new file mode 100644 index 000000000..dc6778409 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java @@ -0,0 +1,131 @@ +package org.citra.citra_emu.features.settings.model; + +import android.text.TextUtils; + +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.ui.SettingsActivityView; +import org.citra.citra_emu.features.settings.utils.SettingsFile; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class Settings { + public static final String SECTION_PREMIUM = "Premium"; + public static final String SECTION_CORE = "Core"; + public static final String SECTION_SYSTEM = "System"; + public static final String SECTION_CAMERA = "Camera"; + public static final String SECTION_CONTROLS = "Controls"; + public static final String SECTION_RENDERER = "Renderer"; + public static final String SECTION_LAYOUT = "Layout"; + public static final String SECTION_AUDIO = "Audio"; + public static final String SECTION_DEBUG = "Debug"; + + private String gameId; + + private static final Map<String, List<String>> configFileSectionsMap = new HashMap<>(); + + static { + configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_PREMIUM, SECTION_CORE, SECTION_SYSTEM, SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, SECTION_LAYOUT, SECTION_AUDIO, SECTION_DEBUG)); + } + + /** + * A HashMap<String, SettingSection> that constructs a new SettingSection instead of returning null + * when getting a key not already in the map + */ + public static final class SettingsSectionMap extends HashMap<String, SettingSection> { + @Override + public SettingSection get(Object key) { + if (!(key instanceof String)) { + return null; + } + + String stringKey = (String) key; + + if (!super.containsKey(stringKey)) { + SettingSection section = new SettingSection(stringKey); + super.put(stringKey, section); + return section; + } + return super.get(key); + } + } + + private HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap(); + + public SettingSection getSection(String sectionName) { + return sections.get(sectionName); + } + + public boolean isEmpty() { + return sections.isEmpty(); + } + + public HashMap<String, SettingSection> getSections() { + return sections; + } + + public void loadSettings(SettingsActivityView view) { + sections = new Settings.SettingsSectionMap(); + loadCitraSettings(view); + + if (!TextUtils.isEmpty(gameId)) { + loadCustomGameSettings(gameId, view); + } + } + + private void loadCitraSettings(SettingsActivityView view) { + for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) { + String fileName = entry.getKey(); + sections.putAll(SettingsFile.readFile(fileName, view)); + } + } + + private void loadCustomGameSettings(String gameId, SettingsActivityView view) { + // custom game settings + mergeSections(SettingsFile.readCustomGameSettings(gameId, view)); + } + + private void mergeSections(HashMap<String, SettingSection> updatedSections) { + for (Map.Entry<String, SettingSection> entry : updatedSections.entrySet()) { + if (sections.containsKey(entry.getKey())) { + SettingSection originalSection = sections.get(entry.getKey()); + SettingSection updatedSection = entry.getValue(); + originalSection.mergeSection(updatedSection); + } else { + sections.put(entry.getKey(), entry.getValue()); + } + } + } + + public void loadSettings(String gameId, SettingsActivityView view) { + this.gameId = gameId; + loadSettings(view); + } + + public void saveSettings(SettingsActivityView view) { + if (TextUtils.isEmpty(gameId)) { + view.showToastMessage(CitraApplication.getAppContext().getString(R.string.ini_saved), false); + + for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) { + String fileName = entry.getKey(); + List<String> sectionNames = entry.getValue(); + TreeMap<String, SettingSection> iniSections = new TreeMap<>(); + for (String section : sectionNames) { + iniSections.put(section, sections.get(section)); + } + + SettingsFile.saveFile(fileName, iniSections, view); + } + } else { + // custom game settings + view.showToastMessage(CitraApplication.getAppContext().getString(R.string.gameid_saved, gameId), false); + + SettingsFile.saveCustomGameSettings(gameId, sections); + } + + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java new file mode 100644 index 000000000..b906b7010 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java @@ -0,0 +1,23 @@ +package org.citra.citra_emu.features.settings.model; + +public final class StringSetting extends Setting { + private String mValue; + + public StringSetting(String key, String section, String value) { + super(key, section); + mValue = value; + } + + public String getValue() { + return mValue; + } + + public void setValue(String value) { + mValue = value; + } + + @Override + public String getValueAsString() { + return mValue; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java new file mode 100644 index 000000000..baf40709f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java @@ -0,0 +1,80 @@ +package org.citra.citra_emu.features.settings.model.view; + +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.BooleanSetting; +import org.citra.citra_emu.features.settings.model.IntSetting; +import org.citra.citra_emu.features.settings.model.Setting; +import org.citra.citra_emu.features.settings.ui.SettingsFragmentView; + +public final class CheckBoxSetting extends SettingsItem { + private boolean mDefaultValue; + private boolean mShowPerformanceWarning; + private SettingsFragmentView mView; + + public CheckBoxSetting(String key, String section, int titleId, int descriptionId, + boolean defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mDefaultValue = defaultValue; + mShowPerformanceWarning = false; + } + + public CheckBoxSetting(String key, String section, int titleId, int descriptionId, + boolean defaultValue, Setting setting, boolean show_performance_warning, SettingsFragmentView view) { + super(key, section, setting, titleId, descriptionId); + mDefaultValue = defaultValue; + mView = view; + mShowPerformanceWarning = show_performance_warning; + } + + public boolean isChecked() { + if (getSetting() == null) { + return mDefaultValue; + } + + // Try integer setting + try { + IntSetting setting = (IntSetting) getSetting(); + return setting.getValue() == 1; + } catch (ClassCastException exception) { + } + + // Try boolean setting + try { + BooleanSetting setting = (BooleanSetting) getSetting(); + return setting.getValue() == true; + } catch (ClassCastException exception) { + } + + return mDefaultValue; + } + + /** + * Write a value to the backing boolean. If that boolean was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param checked Pretty self explanatory. + * @return null if overwritten successfully; otherwise, a newly created BooleanSetting. + */ + public IntSetting setChecked(boolean checked) { + // Show a performance warning if the setting has been disabled + if (mShowPerformanceWarning && !checked) { + mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.performance_warning), true); + } + + if (getSetting() == null) { + IntSetting setting = new IntSetting(getKey(), getSection(), checked ? 1 : 0); + setSetting(setting); + return setting; + } else { + IntSetting setting = (IntSetting) getSetting(); + setting.setValue(checked ? 1 : 0); + return null; + } + } + + @Override + public int getType() { + return TYPE_CHECKBOX; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java new file mode 100644 index 000000000..afc3352cc --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java @@ -0,0 +1,40 @@ +package org.citra.citra_emu.features.settings.model.view; + +import org.citra.citra_emu.features.settings.model.Setting; +import org.citra.citra_emu.features.settings.model.StringSetting; + +public final class DateTimeSetting extends SettingsItem { + private String mDefaultValue; + + public DateTimeSetting(String key, String section, int titleId, int descriptionId, + String defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mDefaultValue = defaultValue; + } + + public String getValue() { + if (getSetting() != null) { + StringSetting setting = (StringSetting) getSetting(); + return setting.getValue(); + } else { + return mDefaultValue; + } + } + + public StringSetting setSelectedValue(String datetime) { + if (getSetting() == null) { + StringSetting setting = new StringSetting(getKey(), getSection(), datetime); + setSetting(setting); + return setting; + } else { + StringSetting setting = (StringSetting) getSetting(); + setting.setValue(datetime); + return null; + } + } + + @Override + public int getType() { + return TYPE_DATETIME_SETTING; + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java new file mode 100644 index 000000000..bac8876cd --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java @@ -0,0 +1,14 @@ +package org.citra.citra_emu.features.settings.model.view; + +import org.citra.citra_emu.features.settings.model.Setting; + +public final class HeaderSetting extends SettingsItem { + public HeaderSetting(String key, Setting setting, int titleId, int descriptionId) { + super(key, null, setting, titleId, descriptionId); + } + + @Override + public int getType() { + return SettingsItem.TYPE_HEADER; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java new file mode 100644 index 000000000..e9141a208 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java @@ -0,0 +1,382 @@ +package org.citra.citra_emu.features.settings.model.view; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.widget.Toast; + +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.Setting; +import org.citra.citra_emu.features.settings.model.StringSetting; +import org.citra.citra_emu.features.settings.utils.SettingsFile; + +public final class InputBindingSetting extends SettingsItem { + private static final String INPUT_MAPPING_PREFIX = "InputMapping"; + + public InputBindingSetting(String key, String section, int titleId, Setting setting) { + super(key, section, setting, titleId, 0); + } + + public String getValue() { + if (getSetting() == null) { + return ""; + } + + StringSetting setting = (StringSetting) getSetting(); + return setting.getValue(); + } + + /** + * Returns true if this key is for the 3DS Circle Pad + */ + private boolean IsCirclePad() { + switch (getKey()) { + case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL: + case SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL: + return true; + } + return false; + } + + /** + * Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad + */ + public boolean IsHorizontalOrientation() { + switch (getKey()) { + case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL: + case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL: + case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL: + return true; + } + return false; + } + + /** + * Returns true if this key is for the 3DS C-Stick + */ + private boolean IsCStick() { + switch (getKey()) { + case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL: + case SettingsFile.KEY_CSTICK_AXIS_VERTICAL: + return true; + } + return false; + } + + /** + * Returns true if this key is for the 3DS D-Pad + */ + private boolean IsDPad() { + switch (getKey()) { + case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL: + case SettingsFile.KEY_DPAD_AXIS_VERTICAL: + return true; + } + return false; + } + + /** + * Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real + * triggers on the 3DS, but we support them as such on a physical gamepad. + */ + public boolean IsTrigger() { + switch (getKey()) { + case SettingsFile.KEY_BUTTON_L: + case SettingsFile.KEY_BUTTON_R: + case SettingsFile.KEY_BUTTON_ZL: + case SettingsFile.KEY_BUTTON_ZR: + return true; + } + return false; + } + + /** + * Returns true if a gamepad axis can be used to map this key. + */ + public boolean IsAxisMappingSupported() { + return IsCirclePad() || IsCStick() || IsDPad() || IsTrigger(); + } + + /** + * Returns true if a gamepad button can be used to map this key. + */ + private boolean IsButtonMappingSupported() { + return !IsAxisMappingSupported() || IsTrigger(); + } + + /** + * Returns the Citra button code for the settings key. + */ + private int getButtonCode() { + switch (getKey()) { + case SettingsFile.KEY_BUTTON_A: + return NativeLibrary.ButtonType.BUTTON_A; + case SettingsFile.KEY_BUTTON_B: + return NativeLibrary.ButtonType.BUTTON_B; + case SettingsFile.KEY_BUTTON_X: + return NativeLibrary.ButtonType.BUTTON_X; + case SettingsFile.KEY_BUTTON_Y: + return NativeLibrary.ButtonType.BUTTON_Y; + case SettingsFile.KEY_BUTTON_L: + return NativeLibrary.ButtonType.TRIGGER_L; + case SettingsFile.KEY_BUTTON_R: + return NativeLibrary.ButtonType.TRIGGER_R; + case SettingsFile.KEY_BUTTON_ZL: + return NativeLibrary.ButtonType.BUTTON_ZL; + case SettingsFile.KEY_BUTTON_ZR: + return NativeLibrary.ButtonType.BUTTON_ZR; + case SettingsFile.KEY_BUTTON_SELECT: + return NativeLibrary.ButtonType.BUTTON_SELECT; + case SettingsFile.KEY_BUTTON_START: + return NativeLibrary.ButtonType.BUTTON_START; + case SettingsFile.KEY_BUTTON_UP: + return NativeLibrary.ButtonType.DPAD_UP; + case SettingsFile.KEY_BUTTON_DOWN: + return NativeLibrary.ButtonType.DPAD_DOWN; + case SettingsFile.KEY_BUTTON_LEFT: + return NativeLibrary.ButtonType.DPAD_LEFT; + case SettingsFile.KEY_BUTTON_RIGHT: + return NativeLibrary.ButtonType.DPAD_RIGHT; + } + return -1; + } + + /** + * Returns the settings key for the specified Citra button code. + */ + private static String getButtonKey(int buttonCode) { + switch (buttonCode) { + case NativeLibrary.ButtonType.BUTTON_A: + return SettingsFile.KEY_BUTTON_A; + case NativeLibrary.ButtonType.BUTTON_B: + return SettingsFile.KEY_BUTTON_B; + case NativeLibrary.ButtonType.BUTTON_X: + return SettingsFile.KEY_BUTTON_X; + case NativeLibrary.ButtonType.BUTTON_Y: + return SettingsFile.KEY_BUTTON_Y; + case NativeLibrary.ButtonType.TRIGGER_L: + return SettingsFile.KEY_BUTTON_L; + case NativeLibrary.ButtonType.TRIGGER_R: + return SettingsFile.KEY_BUTTON_R; + case NativeLibrary.ButtonType.BUTTON_ZL: + return SettingsFile.KEY_BUTTON_ZL; + case NativeLibrary.ButtonType.BUTTON_ZR: + return SettingsFile.KEY_BUTTON_ZR; + case NativeLibrary.ButtonType.BUTTON_SELECT: + return SettingsFile.KEY_BUTTON_SELECT; + case NativeLibrary.ButtonType.BUTTON_START: + return SettingsFile.KEY_BUTTON_START; + case NativeLibrary.ButtonType.DPAD_UP: + return SettingsFile.KEY_BUTTON_UP; + case NativeLibrary.ButtonType.DPAD_DOWN: + return SettingsFile.KEY_BUTTON_DOWN; + case NativeLibrary.ButtonType.DPAD_LEFT: + return SettingsFile.KEY_BUTTON_LEFT; + case NativeLibrary.ButtonType.DPAD_RIGHT: + return SettingsFile.KEY_BUTTON_RIGHT; + } + return ""; + } + + /** + * Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old + * settings on re-mapping or clearing of a setting. + */ + private String getReverseKey() { + String reverseKey = INPUT_MAPPING_PREFIX + "_ReverseMapping_" + getKey(); + + if (IsAxisMappingSupported() && !IsTrigger()) { + // Triggers are the only axis-supported mappings without orientation + reverseKey += "_" + (IsHorizontalOrientation() ? 0 : 1); + } + + return reverseKey; + } + + /** + * Removes the old mapping for this key from the settings, e.g. on user clearing the setting. + */ + public void removeOldMapping() { + // Get preferences editor + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); + SharedPreferences.Editor editor = preferences.edit(); + + // Try remove all possible keys we wrote for this setting + String oldKey = preferences.getString(getReverseKey(), ""); + if (!oldKey.equals("")) { + editor.remove(getKey()); // Used for ui text + editor.remove(oldKey); // Used for button mapping + editor.remove(oldKey + "_GuestOrientation"); // Used for axis orientation + editor.remove(oldKey + "_GuestButton"); // Used for axis button + } + + // Apply changes + editor.apply(); + } + + /** + * Helper function to get the settings key for an gamepad button. + */ + public static String getInputButtonKey(int keyCode) { + return INPUT_MAPPING_PREFIX + "_Button_" + keyCode; + } + + /** + * Helper function to get the settings key for an gamepad axis. + */ + public static String getInputAxisKey(int axis) { + return INPUT_MAPPING_PREFIX + "_HostAxis_" + axis; + } + + /** + * Helper function to get the settings key for an gamepad axis button (stick or trigger). + */ + public static String getInputAxisButtonKey(int axis) { + return getInputAxisKey(axis) + "_GuestButton"; + } + + /** + * Helper function to get the settings key for an gamepad axis orientation. + */ + public static String getInputAxisOrientationKey(int axis) { + return getInputAxisKey(axis) + "_GuestOrientation"; + } + + /** + * Helper function to write a gamepad button mapping for the setting. + */ + private void WriteButtonMapping(String key) { + // Get preferences editor + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); + SharedPreferences.Editor editor = preferences.edit(); + + // Remove mapping for another setting using this input + int oldButtonCode = preferences.getInt(key, -1); + if (oldButtonCode != -1) { + String oldKey = getButtonKey(oldButtonCode); + editor.remove(oldKey); // Only need to remove UI text setting, others will be overwritten + } + + // Cleanup old mapping for this setting + removeOldMapping(); + + // Write new mapping + editor.putInt(key, getButtonCode()); + + // Write next reverse mapping for future cleanup + editor.putString(getReverseKey(), key); + + // Apply changes + editor.apply(); + } + + /** + * Helper function to write a gamepad axis mapping for the setting. + */ + private void WriteAxisMapping(int axis, int value) { + // Get preferences editor + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); + SharedPreferences.Editor editor = preferences.edit(); + + // Cleanup old mapping + removeOldMapping(); + + // Write new mapping + editor.putInt(getInputAxisOrientationKey(axis), IsHorizontalOrientation() ? 0 : 1); + editor.putInt(getInputAxisButtonKey(axis), value); + + // Write next reverse mapping for future cleanup + editor.putString(getReverseKey(), getInputAxisKey(axis)); + + // Apply changes + editor.apply(); + } + + /** + * Saves the provided key input setting as an Android preference. + * + * @param keyEvent KeyEvent of this key press. + */ + public void onKeyInput(KeyEvent keyEvent) { + if (!IsButtonMappingSupported()) { + Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show(); + return; + } + + InputDevice device = keyEvent.getDevice(); + + WriteButtonMapping(getInputButtonKey(keyEvent.getKeyCode())); + + String uiString = device.getName() + ": Button " + keyEvent.getKeyCode(); + setUiString(uiString); + } + + /** + * Saves the provided motion input setting as an Android preference. + * + * @param device InputDevice from which the input event originated. + * @param motionRange MotionRange of the movement + * @param axisDir Either '-' or '+' (currently unused) + */ + public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange, + char axisDir) { + if (!IsAxisMappingSupported()) { + Toast.makeText(CitraApplication.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show(); + return; + } + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); + SharedPreferences.Editor editor = preferences.edit(); + + int button; + if (IsCirclePad()) { + button = NativeLibrary.ButtonType.STICK_LEFT; + } else if (IsCStick()) { + button = NativeLibrary.ButtonType.STICK_C; + } else if (IsDPad()) { + button = NativeLibrary.ButtonType.DPAD; + } else { + button = getButtonCode(); + } + + WriteAxisMapping(motionRange.getAxis(), button); + + String uiString = device.getName() + ": Axis " + motionRange.getAxis(); + setUiString(uiString); + + editor.apply(); + } + + /** + * Sets the string to use in the configuration UI for the gamepad input. + */ + private StringSetting setUiString(String ui) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); + SharedPreferences.Editor editor = preferences.edit(); + + if (getSetting() == null) { + StringSetting setting = new StringSetting(getKey(), getSection(), ""); + setSetting(setting); + + editor.putString(setting.getKey(), ui); + editor.apply(); + + return setting; + } else { + StringSetting setting = (StringSetting) getSetting(); + + editor.putString(setting.getKey(), ui); + editor.apply(); + + return null; + } + } + + @Override + public int getType() { + return TYPE_INPUT_BINDING; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumHeader.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumHeader.java new file mode 100644 index 000000000..8942bf13a --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumHeader.java @@ -0,0 +1,14 @@ +package org.citra.citra_emu.features.settings.model.view; + +import org.citra.citra_emu.features.settings.model.Setting; + +public final class PremiumHeader extends SettingsItem { + public PremiumHeader() { + super(null, null, null, 0, 0); + } + + @Override + public int getType() { + return SettingsItem.TYPE_PREMIUM; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumSingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumSingleChoiceSetting.java new file mode 100644 index 000000000..c0560d2dc --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/PremiumSingleChoiceSetting.java @@ -0,0 +1,59 @@ +package org.citra.citra_emu.features.settings.model.view; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.Setting; +import org.citra.citra_emu.features.settings.ui.SettingsFragmentView; + +public final class PremiumSingleChoiceSetting extends SettingsItem { + private int mDefaultValue; + + private int mChoicesId; + private int mValuesId; + private SettingsFragmentView mView; + + private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); + + public PremiumSingleChoiceSetting(String key, String section, int titleId, int descriptionId, + int choicesId, int valuesId, int defaultValue, Setting setting, SettingsFragmentView view) { + super(key, section, setting, titleId, descriptionId); + mValuesId = valuesId; + mChoicesId = choicesId; + mDefaultValue = defaultValue; + mView = view; + } + + public int getChoicesId() { + return mChoicesId; + } + + public int getValuesId() { + return mValuesId; + } + + public int getSelectedValue() { + return mPreferences.getInt(getKey(), mDefaultValue); + } + + /** + * Write a value to the backing int. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return null if overwritten successfully otherwise; a newly created IntSetting. + */ + public void setSelectedValue(int selection) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putInt(getKey(), selection); + editor.apply(); + mView.showToastMessage(CitraApplication.getAppContext().getString(R.string.design_updated), false); + } + + @Override + public int getType() { + return TYPE_SINGLE_CHOICE; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java new file mode 100644 index 000000000..305352022 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java @@ -0,0 +1,107 @@ +package org.citra.citra_emu.features.settings.model.view; + +import org.citra.citra_emu.features.settings.model.Setting; +import org.citra.citra_emu.features.settings.model.Settings; +import org.citra.citra_emu.features.settings.ui.SettingsAdapter; + +/** + * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. + * Each one corresponds to a {@link Setting} object, so this class's subclasses + * should vaguely correspond to those subclasses. There are a few with multiple analogues + * and a few with none (Headers, for example, do not correspond to anything in the ini + * file.) + */ +public abstract class SettingsItem { + public static final int TYPE_HEADER = 0; + public static final int TYPE_CHECKBOX = 1; + public static final int TYPE_SINGLE_CHOICE = 2; + public static final int TYPE_SLIDER = 3; + public static final int TYPE_SUBMENU = 4; + public static final int TYPE_INPUT_BINDING = 5; + public static final int TYPE_STRING_SINGLE_CHOICE = 6; + public static final int TYPE_DATETIME_SETTING = 7; + public static final int TYPE_PREMIUM = 8; + + private String mKey; + private String mSection; + + private Setting mSetting; + + private int mNameId; + private int mDescriptionId; + private boolean mIsPremium; + + /** + * Base constructor. Takes a key / section name in case the third parameter, the Setting, + * is null; in which case, one can be constructed and saved using the key / section. + * + * @param key Identifier for the Setting represented by this Item. + * @param section Section to which the Setting belongs. + * @param setting A possibly-null backing Setting, to be modified on UI events. + * @param nameId Resource ID for a text string to be displayed as this setting's name. + * @param descriptionId Resource ID for a text string to be displayed as this setting's description. + */ + public SettingsItem(String key, String section, Setting setting, int nameId, + int descriptionId) { + mKey = key; + mSection = section; + mSetting = setting; + mNameId = nameId; + mDescriptionId = descriptionId; + mIsPremium = (section == Settings.SECTION_PREMIUM); + } + + /** + * @return The identifier for the backing Setting. + */ + public String getKey() { + return mKey; + } + + /** + * @return The header under which the backing Setting belongs. + */ + public String getSection() { + return mSection; + } + + /** + * @return The backing Setting, possibly null. + */ + public Setting getSetting() { + return mSetting; + } + + /** + * Replace the backing setting with a new one. Generally used in cases where + * the backing setting is null. + * + * @param setting A non-null Setting. + */ + public void setSetting(Setting setting) { + mSetting = setting; + } + + /** + * @return A resource ID for a text string representing this Setting's name. + */ + public int getNameId() { + return mNameId; + } + + public int getDescriptionId() { + return mDescriptionId; + } + + public boolean isPremium() { + return mIsPremium; + } + + /** + * Used by {@link SettingsAdapter}'s onCreateViewHolder() + * method to determine which type of ViewHolder should be created. + * + * @return An integer (ideally, one of the constants defined in this file) + */ + public abstract int getType(); +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java new file mode 100644 index 000000000..ee9d225d6 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java @@ -0,0 +1,60 @@ +package org.citra.citra_emu.features.settings.model.view; + +import org.citra.citra_emu.features.settings.model.IntSetting; +import org.citra.citra_emu.features.settings.model.Setting; + +public final class SingleChoiceSetting extends SettingsItem { + private int mDefaultValue; + + private int mChoicesId; + private int mValuesId; + + public SingleChoiceSetting(String key, String section, int titleId, int descriptionId, + int choicesId, int valuesId, int defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mValuesId = valuesId; + mChoicesId = choicesId; + mDefaultValue = defaultValue; + } + + public int getChoicesId() { + return mChoicesId; + } + + public int getValuesId() { + return mValuesId; + } + + public int getSelectedValue() { + if (getSetting() != null) { + IntSetting setting = (IntSetting) getSetting(); + return setting.getValue(); + } else { + return mDefaultValue; + } + } + + /** + * Write a value to the backing int. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return null if overwritten successfully otherwise; a newly created IntSetting. + */ + public IntSetting setSelectedValue(int selection) { + if (getSetting() == null) { + IntSetting setting = new IntSetting(getKey(), getSection(), selection); + setSetting(setting); + return setting; + } else { + IntSetting setting = (IntSetting) getSetting(); + setting.setValue(selection); + return null; + } + } + + @Override + public int getType() { + return TYPE_SINGLE_CHOICE; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java new file mode 100644 index 000000000..551b13f99 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java @@ -0,0 +1,101 @@ +package org.citra.citra_emu.features.settings.model.view; + +import org.citra.citra_emu.features.settings.model.FloatSetting; +import org.citra.citra_emu.features.settings.model.IntSetting; +import org.citra.citra_emu.features.settings.model.Setting; +import org.citra.citra_emu.utils.Log; + +public final class SliderSetting extends SettingsItem { + private int mMin; + private int mMax; + private int mDefaultValue; + + private String mUnits; + + public SliderSetting(String key, String section, int titleId, int descriptionId, + int min, int max, String units, int defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mMin = min; + mMax = max; + mUnits = units; + mDefaultValue = defaultValue; + } + + public int getMin() { + return mMin; + } + + public int getMax() { + return mMax; + } + + public int getDefaultValue() { + return mDefaultValue; + } + + public int getSelectedValue() { + Setting setting = getSetting(); + + if (setting == null) { + return mDefaultValue; + } + + if (setting instanceof IntSetting) { + IntSetting intSetting = (IntSetting) setting; + return intSetting.getValue(); + } else if (setting instanceof FloatSetting) { + FloatSetting floatSetting = (FloatSetting) setting; + return Math.round(floatSetting.getValue()); + } else { + Log.error("[SliderSetting] Error casting setting type."); + return -1; + } + } + + /** + * Write a value to the backing int. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return null if overwritten successfully otherwise; a newly created IntSetting. + */ + public IntSetting setSelectedValue(int selection) { + if (getSetting() == null) { + IntSetting setting = new IntSetting(getKey(), getSection(), selection); + setSetting(setting); + return setting; + } else { + IntSetting setting = (IntSetting) getSetting(); + setting.setValue(selection); + return null; + } + } + + /** + * Write a value to the backing float. If that float was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the float. + * @return null if overwritten successfully otherwise; a newly created FloatSetting. + */ + public FloatSetting setSelectedValue(float selection) { + if (getSetting() == null) { + FloatSetting setting = new FloatSetting(getKey(), getSection(), selection); + setSetting(setting); + return setting; + } else { + FloatSetting setting = (FloatSetting) getSetting(); + setting.setValue(selection); + return null; + } + } + + public String getUnits() { + return mUnits; + } + + @Override + public int getType() { + return TYPE_SLIDER; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java new file mode 100644 index 000000000..057145d9d --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java @@ -0,0 +1,82 @@ +package org.citra.citra_emu.features.settings.model.view; + +import org.citra.citra_emu.features.settings.model.Setting; +import org.citra.citra_emu.features.settings.model.StringSetting; + +public class StringSingleChoiceSetting extends SettingsItem { + private String mDefaultValue; + + private String[] mChoicesId; + private String[] mValuesId; + + public StringSingleChoiceSetting(String key, String section, int titleId, int descriptionId, + String[] choicesId, String[] valuesId, String defaultValue, Setting setting) { + super(key, section, setting, titleId, descriptionId); + mValuesId = valuesId; + mChoicesId = choicesId; + mDefaultValue = defaultValue; + } + + public String[] getChoicesId() { + return mChoicesId; + } + + public String[] getValuesId() { + return mValuesId; + } + + public String getValueAt(int index) { + if (mValuesId == null) + return null; + + if (index >= 0 && index < mValuesId.length) { + return mValuesId[index]; + } + + return ""; + } + + public String getSelectedValue() { + if (getSetting() != null) { + StringSetting setting = (StringSetting) getSetting(); + return setting.getValue(); + } else { + return mDefaultValue; + } + } + + public int getSelectValueIndex() { + String selectedValue = getSelectedValue(); + for (int i = 0; i < mValuesId.length; i++) { + if (mValuesId[i].equals(selectedValue)) { + return i; + } + } + + return -1; + } + + /** + * Write a value to the backing int. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return null if overwritten successfully otherwise; a newly created IntSetting. + */ + public StringSetting setSelectedValue(String selection) { + if (getSetting() == null) { + StringSetting setting = new StringSetting(getKey(), getSection(), selection); + setSetting(setting); + return setting; + } else { + StringSetting setting = (StringSetting) getSetting(); + setting.setValue(selection); + return null; + } + } + + @Override + public int getType() { + return TYPE_STRING_SINGLE_CHOICE; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java new file mode 100644 index 000000000..9d44a923f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java @@ -0,0 +1,21 @@ +package org.citra.citra_emu.features.settings.model.view; + +import org.citra.citra_emu.features.settings.model.Setting; + +public final class SubmenuSetting extends SettingsItem { + private String mMenuKey; + + public SubmenuSetting(String key, Setting setting, int titleId, int descriptionId, String menuKey) { + super(key, null, setting, titleId, descriptionId); + mMenuKey = menuKey; + } + + public String getMenuKey() { + return mMenuKey; + } + + @Override + public int getType() { + return TYPE_SUBMENU; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java new file mode 100644 index 000000000..23c3c4c9e --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java @@ -0,0 +1,215 @@ +package org.citra.citra_emu.features.settings.ui; + +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.provider.Settings; +import android.view.Menu; +import android.view.MenuInflater; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentTransaction; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.R; +import org.citra.citra_emu.utils.DirectoryInitialization; +import org.citra.citra_emu.utils.DirectoryStateReceiver; +import org.citra.citra_emu.utils.EmulationMenuSettings; + +public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView { + private static final String ARG_MENU_TAG = "menu_tag"; + private static final String ARG_GAME_ID = "game_id"; + private static final String FRAGMENT_TAG = "settings"; + private SettingsActivityPresenter mPresenter = new SettingsActivityPresenter(this); + + private ProgressDialog dialog; + + public static void launch(Context context, String menuTag, String gameId) { + Intent settings = new Intent(context, SettingsActivity.class); + settings.putExtra(ARG_MENU_TAG, menuTag); + settings.putExtra(ARG_GAME_ID, gameId); + context.startActivity(settings); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_settings); + + Intent launcher = getIntent(); + String gameID = launcher.getStringExtra(ARG_GAME_ID); + String menuTag = launcher.getStringExtra(ARG_MENU_TAG); + + mPresenter.onCreate(savedInstanceState, menuTag, gameID); + + // Show "Back" button in the action bar for navigation + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + + return true; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_settings, menu); + + return true; + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + // Critical: If super method is not called, rotations will be busted. + super.onSaveInstanceState(outState); + mPresenter.saveState(outState); + } + + @Override + protected void onStart() { + super.onStart(); + mPresenter.onStart(); + } + + /** + * If this is called, the user has left the settings screen (potentially through the + * home button) and will expect their changes to be persisted. So we kick off an + * IntentService which will do so on a background thread. + */ + @Override + protected void onStop() { + super.onStop(); + + mPresenter.onStop(isFinishing()); + + // Update framebuffer layout when closing the settings + NativeLibrary.NotifyOrientationChange(EmulationMenuSettings.getLandscapeScreenLayout(), + getWindowManager().getDefaultDisplay().getRotation()); + } + + @Override + public void showSettingsFragment(String menuTag, boolean addToStack, String gameID) { + if (!addToStack && getFragment() != null) { + return; + } + + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + + if (addToStack) { + if (areSystemAnimationsEnabled()) { + transaction.setCustomAnimations( + R.animator.settings_enter, + R.animator.settings_exit, + R.animator.settings_pop_enter, + R.animator.setttings_pop_exit); + } + + transaction.addToBackStack(null); + } + transaction.replace(R.id.frame_content, SettingsFragment.newInstance(menuTag, gameID), FRAGMENT_TAG); + + transaction.commit(); + } + + private boolean areSystemAnimationsEnabled() { + float duration = Settings.Global.getFloat( + getContentResolver(), + Settings.Global.ANIMATOR_DURATION_SCALE, 1); + float transition = Settings.Global.getFloat( + getContentResolver(), + Settings.Global.TRANSITION_ANIMATION_SCALE, 1); + return duration != 0 && transition != 0; + } + + @Override + public void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter) { + LocalBroadcastManager.getInstance(this).registerReceiver( + receiver, + filter); + DirectoryInitialization.start(this); + } + + @Override + public void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver) { + LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver); + } + + @Override + public void showLoading() { + if (dialog == null) { + dialog = new ProgressDialog(this); + dialog.setMessage(getString(R.string.load_settings)); + dialog.setIndeterminate(true); + } + + dialog.show(); + } + + @Override + public void hideLoading() { + dialog.dismiss(); + } + + @Override + public void showPermissionNeededHint() { + Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT) + .show(); + } + + @Override + public void showExternalStorageNotMountedHint() { + Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT) + .show(); + } + + @Override + public org.citra.citra_emu.features.settings.model.Settings getSettings() { + return mPresenter.getSettings(); + } + + @Override + public void setSettings(org.citra.citra_emu.features.settings.model.Settings settings) { + mPresenter.setSettings(settings); + } + + @Override + public void onSettingsFileLoaded(org.citra.citra_emu.features.settings.model.Settings settings) { + SettingsFragmentView fragment = getFragment(); + + if (fragment != null) { + fragment.onSettingsFileLoaded(settings); + } + } + + @Override + public void onSettingsFileNotFound() { + SettingsFragmentView fragment = getFragment(); + + if (fragment != null) { + fragment.loadDefaultSettings(); + } + } + + @Override + public void showToastMessage(String message, boolean is_long) { + Toast.makeText(this, message, is_long ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); + } + + @Override + public void onSettingChanged() { + mPresenter.onSettingChanged(); + } + + private SettingsFragment getFragment() { + return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java new file mode 100644 index 000000000..0d63873bb --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java @@ -0,0 +1,124 @@ +package org.citra.citra_emu.features.settings.ui; + +import android.content.IntentFilter; +import android.os.Bundle; +import android.text.TextUtils; + +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.features.settings.model.Settings; +import org.citra.citra_emu.features.settings.utils.SettingsFile; +import org.citra.citra_emu.utils.DirectoryInitialization; +import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState; +import org.citra.citra_emu.utils.DirectoryStateReceiver; +import org.citra.citra_emu.utils.Log; +import org.citra.citra_emu.utils.ThemeUtil; + +import java.io.File; + +public final class SettingsActivityPresenter { + private static final String KEY_SHOULD_SAVE = "should_save"; + + private SettingsActivityView mView; + + private Settings mSettings = new Settings(); + + private boolean mShouldSave; + + private DirectoryStateReceiver directoryStateReceiver; + + private String menuTag; + private String gameId; + + public SettingsActivityPresenter(SettingsActivityView view) { + mView = view; + } + + public void onCreate(Bundle savedInstanceState, String menuTag, String gameId) { + if (savedInstanceState == null) { + this.menuTag = menuTag; + this.gameId = gameId; + } else { + mShouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE); + } + } + + public void onStart() { + prepareCitraDirectoriesIfNeeded(); + } + + void loadSettingsUI() { + if (mSettings.isEmpty()) { + if (!TextUtils.isEmpty(gameId)) { + mSettings.loadSettings(gameId, mView); + } else { + mSettings.loadSettings(mView); + } + } + + mView.showSettingsFragment(menuTag, false, gameId); + mView.onSettingsFileLoaded(mSettings); + } + + private void prepareCitraDirectoriesIfNeeded() { + File configFile = new File(DirectoryInitialization.getUserDirectory() + "/config/" + SettingsFile.FILE_NAME_CONFIG + ".ini"); + if (!configFile.exists()) { + Log.error("Citra config file could not be found!"); + } + if (DirectoryInitialization.areCitraDirectoriesReady()) { + loadSettingsUI(); + } else { + mView.showLoading(); + IntentFilter statusIntentFilter = new IntentFilter( + DirectoryInitialization.BROADCAST_ACTION); + + directoryStateReceiver = + new DirectoryStateReceiver(directoryInitializationState -> + { + if (directoryInitializationState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) { + mView.hideLoading(); + loadSettingsUI(); + } else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) { + mView.showPermissionNeededHint(); + mView.hideLoading(); + } else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { + mView.showExternalStorageNotMountedHint(); + mView.hideLoading(); + } + }); + + mView.startDirectoryInitializationService(directoryStateReceiver, statusIntentFilter); + } + } + + public void setSettings(Settings settings) { + mSettings = settings; + } + + public Settings getSettings() { + return mSettings; + } + + public void onStop(boolean finishing) { + if (directoryStateReceiver != null) { + mView.stopListeningToDirectoryInitializationService(directoryStateReceiver); + directoryStateReceiver = null; + } + + if (mSettings != null && finishing && mShouldSave) { + Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI..."); + mSettings.saveSettings(mView); + } + + ThemeUtil.applyTheme(); + + NativeLibrary.ReloadSettings(); + } + + public void onSettingChanged() { + mShouldSave = true; + } + + public void saveState(Bundle outState) { + outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java new file mode 100644 index 000000000..0d26d48a7 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java @@ -0,0 +1,103 @@ +package org.citra.citra_emu.features.settings.ui; + +import android.content.IntentFilter; + +import org.citra.citra_emu.features.settings.model.Settings; +import org.citra.citra_emu.utils.DirectoryStateReceiver; + +/** + * Abstraction for the Activity that manages SettingsFragments. + */ +public interface SettingsActivityView { + /** + * Show a new SettingsFragment. + * + * @param menuTag Identifier for the settings group that should be displayed. + * @param addToStack Whether or not this fragment should replace a previous one. + */ + void showSettingsFragment(String menuTag, boolean addToStack, String gameId); + + /** + * Called by a contained Fragment to get access to the Setting HashMap + * loaded from disk, so that each Fragment doesn't need to perform its own + * read operation. + * + * @return A possibly null HashMap of Settings. + */ + Settings getSettings(); + + /** + * Used to provide the Activity with Settings HashMaps if a Fragment already + * has one; for example, if a rotation occurs, the Fragment will not be killed, + * but the Activity will, so the Activity needs to have its HashMaps resupplied. + * + * @param settings The ArrayList of all the Settings HashMaps. + */ + void setSettings(Settings settings); + + /** + * Called when an asynchronous load operation completes. + * + * @param settings The (possibly null) result of the ini load operation. + */ + void onSettingsFileLoaded(Settings settings); + + /** + * Called when an asynchronous load operation fails. + */ + void onSettingsFileNotFound(); + + /** + * Display a popup text message on screen. + * + * @param message The contents of the onscreen message. + * @param is_long Whether this should be a long Toast or short one. + */ + void showToastMessage(String message, boolean is_long); + + /** + * End the activity. + */ + void finish(); + + /** + * Called by a containing Fragment to tell the Activity that a setting was changed; + * unless this has been called, the Activity will not save to disk. + */ + void onSettingChanged(); + + /** + * Show loading dialog while loading the settings + */ + void showLoading(); + + /** + * Hide the loading the dialog + */ + void hideLoading(); + + /** + * Show a hint to the user that the app needs write to external storage access + */ + void showPermissionNeededHint(); + + /** + * Show a hint to the user that the app needs the external storage to be mounted + */ + void showExternalStorageNotMountedHint(); + + /** + * Start the DirectoryInitialization and listen for the result. + * + * @param receiver the broadcast receiver for the DirectoryInitialization + * @param filter the Intent broadcasts to be received. + */ + void startDirectoryInitializationService(DirectoryStateReceiver receiver, IntentFilter filter); + + /** + * Stop listening to the DirectoryInitialization. + * + * @param receiver The broadcast receiver to unregister. + */ + void stopListeningToDirectoryInitializationService(DirectoryStateReceiver receiver); +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java new file mode 100644 index 000000000..bfd7c71a9 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java @@ -0,0 +1,487 @@ +package org.citra.citra_emu.features.settings.ui; + +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.DatePicker; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.TimePicker; + +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.RecyclerView; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.dialogs.MotionAlertDialog; +import org.citra.citra_emu.features.settings.model.FloatSetting; +import org.citra.citra_emu.features.settings.model.IntSetting; +import org.citra.citra_emu.features.settings.model.StringSetting; +import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; +import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; +import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; +import org.citra.citra_emu.features.settings.model.view.SliderSetting; +import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; +import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; +import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHolder; +import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder; +import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder; +import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder; +import org.citra.citra_emu.features.settings.ui.viewholder.PremiumViewHolder; +import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder; +import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder; +import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder; +import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder; +import org.citra.citra_emu.ui.main.MainActivity; +import org.citra.citra_emu.utils.Log; + +import java.util.ArrayList; + +public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder> + implements DialogInterface.OnClickListener, SeekBar.OnSeekBarChangeListener { + private SettingsFragmentView mView; + private Context mContext; + private ArrayList<SettingsItem> mSettings; + + private SettingsItem mClickedItem; + private int mClickedPosition; + private int mSeekbarProgress; + + private AlertDialog mDialog; + private TextView mTextSliderValue; + + public SettingsAdapter(SettingsFragmentView view, Context context) { + mView = view; + mContext = context; + mClickedPosition = -1; + } + + @Override + public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view; + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + + switch (viewType) { + case SettingsItem.TYPE_HEADER: + view = inflater.inflate(R.layout.list_item_settings_header, parent, false); + return new HeaderViewHolder(view, this); + + case SettingsItem.TYPE_CHECKBOX: + view = inflater.inflate(R.layout.list_item_setting_checkbox, parent, false); + return new CheckBoxSettingViewHolder(view, this); + + case SettingsItem.TYPE_SINGLE_CHOICE: + case SettingsItem.TYPE_STRING_SINGLE_CHOICE: + view = inflater.inflate(R.layout.list_item_setting, parent, false); + return new SingleChoiceViewHolder(view, this); + + case SettingsItem.TYPE_SLIDER: + view = inflater.inflate(R.layout.list_item_setting, parent, false); + return new SliderViewHolder(view, this); + + case SettingsItem.TYPE_SUBMENU: + view = inflater.inflate(R.layout.list_item_setting, parent, false); + return new SubmenuViewHolder(view, this); + + case SettingsItem.TYPE_INPUT_BINDING: + view = inflater.inflate(R.layout.list_item_setting, parent, false); + return new InputBindingSettingViewHolder(view, this, mContext); + + case SettingsItem.TYPE_DATETIME_SETTING: + view = inflater.inflate(R.layout.list_item_setting, parent, false); + return new DateTimeViewHolder(view, this); + + case SettingsItem.TYPE_PREMIUM: + view = inflater.inflate(R.layout.premium_item_setting, parent, false); + return new PremiumViewHolder(view, this, mView); + + default: + Log.error("[SettingsAdapter] Invalid view type: " + viewType); + return null; + } + } + + @Override + public void onBindViewHolder(SettingViewHolder holder, int position) { + holder.bind(getItem(position)); + } + + private SettingsItem getItem(int position) { + return mSettings.get(position); + } + + @Override + public int getItemCount() { + if (mSettings != null) { + return mSettings.size(); + } else { + return 0; + } + } + + @Override + public int getItemViewType(int position) { + return getItem(position).getType(); + } + + public void setSettings(ArrayList<SettingsItem> settings) { + mSettings = settings; + notifyDataSetChanged(); + } + + public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) { + IntSetting setting = item.setChecked(checked); + notifyItemChanged(position); + + if (setting != null) { + mView.putSetting(setting); + } + + mView.onSettingChanged(); + } + + public void onSingleChoiceClick(PremiumSingleChoiceSetting item) { + mClickedItem = item; + + int value = getSelectionForSingleChoiceValue(item); + + AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); + + builder.setTitle(item.getNameId()); + builder.setSingleChoiceItems(item.getChoicesId(), value, this); + + mDialog = builder.show(); + } + + public void onSingleChoiceClick(SingleChoiceSetting item) { + mClickedItem = item; + + int value = getSelectionForSingleChoiceValue(item); + + AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); + + builder.setTitle(item.getNameId()); + builder.setSingleChoiceItems(item.getChoicesId(), value, this); + + mDialog = builder.show(); + } + + public void onSingleChoiceClick(SingleChoiceSetting item, int position) { + mClickedPosition = position; + + if (!item.isPremium() || MainActivity.isPremiumActive()) { + // Setting is either not Premium, or the user has Premium + onSingleChoiceClick(item); + return; + } + + // User needs Premium, invoke the billing flow + MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item)); + } + + public void onSingleChoiceClick(PremiumSingleChoiceSetting item, int position) { + mClickedPosition = position; + + if (!item.isPremium() || MainActivity.isPremiumActive()) { + // Setting is either not Premium, or the user has Premium + onSingleChoiceClick(item); + return; + } + + // User needs Premium, invoke the billing flow + MainActivity.invokePremiumBilling(() -> onSingleChoiceClick(item)); + } + + public void onStringSingleChoiceClick(StringSingleChoiceSetting item) { + mClickedItem = item; + + AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); + + builder.setTitle(item.getNameId()); + builder.setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this); + + mDialog = builder.show(); + } + + public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) { + mClickedPosition = position; + + if (!item.isPremium() || MainActivity.isPremiumActive()) { + // Setting is either not Premium, or the user has Premium + onStringSingleChoiceClick(item); + return; + } + + // User needs Premium, invoke the billing flow + MainActivity.invokePremiumBilling(() -> onStringSingleChoiceClick(item)); + } + + DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog(); + + public void onDateTimeClick(DateTimeSetting item, int position) { + mClickedItem = item; + mClickedPosition = position; + + AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); + + LayoutInflater inflater = LayoutInflater.from(mView.getActivity()); + View view = inflater.inflate(R.layout.sysclock_datetime_picker, null); + + DatePicker dp = view.findViewById(R.id.date_picker); + TimePicker tp = view.findViewById(R.id.time_picker); + + //set date and time to substrings of settingValue; format = 2018-12-24 04:20:69 (alright maybe not that 69) + String settingValue = item.getValue(); + dp.updateDate(Integer.parseInt(settingValue.substring(0, 4)), Integer.parseInt(settingValue.substring(5, 7)) - 1, Integer.parseInt(settingValue.substring(8, 10))); + + tp.setIs24HourView(true); + tp.setHour(Integer.parseInt(settingValue.substring(11, 13))); + tp.setMinute(Integer.parseInt(settingValue.substring(14, 16))); + + DialogInterface.OnClickListener ok = (dialog, which) -> { + //set it + int year = dp.getYear(); + if (year < 2000) { + year = 2000; + } + String month = ("00" + (dp.getMonth() + 1)).substring(String.valueOf(dp.getMonth() + 1).length()); + String day = ("00" + dp.getDayOfMonth()).substring(String.valueOf(dp.getDayOfMonth()).length()); + String hr = ("00" + tp.getHour()).substring(String.valueOf(tp.getHour()).length()); + String min = ("00" + tp.getMinute()).substring(String.valueOf(tp.getMinute()).length()); + String datetime = year + "-" + month + "-" + day + " " + hr + ":" + min + ":01"; + + StringSetting setting = item.setSelectedValue(datetime); + if (setting != null) { + mView.putSetting(setting); + } + + mView.onSettingChanged(); + + mClickedItem = null; + closeDialog(); + }; + + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, ok); + builder.setNegativeButton(android.R.string.cancel, defaultCancelListener); + mDialog = builder.show(); + } + + public void onSliderClick(SliderSetting item, int position) { + mClickedItem = item; + mClickedPosition = position; + mSeekbarProgress = item.getSelectedValue(); + AlertDialog.Builder builder = new AlertDialog.Builder(mView.getActivity()); + + LayoutInflater inflater = LayoutInflater.from(mView.getActivity()); + View view = inflater.inflate(R.layout.dialog_seekbar, null); + + SeekBar seekbar = view.findViewById(R.id.seekbar); + + builder.setTitle(item.getNameId()); + builder.setView(view); + builder.setPositiveButton(android.R.string.ok, this); + builder.setNegativeButton(android.R.string.cancel, defaultCancelListener); + builder.setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> { + seekbar.setProgress(item.getDefaultValue()); + onClick(dialog, which); + }); + mDialog = builder.show(); + + mTextSliderValue = view.findViewById(R.id.text_value); + mTextSliderValue.setText(String.valueOf(mSeekbarProgress)); + + TextView units = view.findViewById(R.id.text_units); + units.setText(item.getUnits()); + + seekbar.setMin(item.getMin()); + seekbar.setMax(item.getMax()); + seekbar.setProgress(mSeekbarProgress); + + seekbar.setOnSeekBarChangeListener(this); + } + + public void onSubmenuClick(SubmenuSetting item) { + mView.loadSubMenu(item.getMenuKey()); + } + + public void onInputBindingClick(final InputBindingSetting item, final int position) { + final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item); + dialog.setTitle(R.string.input_binding); + + int messageResId = R.string.input_binding_description; + if (item.IsAxisMappingSupported() && !item.IsTrigger()) { + // Use specialized message for axis left/right or up/down + if (item.IsHorizontalOrientation()) { + messageResId = R.string.input_binding_description_horizontal_axis; + } else { + messageResId = R.string.input_binding_description_vertical_axis; + } + } + + dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId()))); + dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), this); + dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) -> + item.removeOldMapping()); + dialog.setOnDismissListener(dialog1 -> + { + StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getValue()); + notifyItemChanged(position); + + mView.putSetting(setting); + + mView.onSettingChanged(); + }); + dialog.setCanceledOnTouchOutside(false); + dialog.show(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (mClickedItem instanceof SingleChoiceSetting) { + SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem; + + int value = getValueForSingleChoiceSelection(scSetting, which); + if (scSetting.getSelectedValue() != value) { + mView.onSettingChanged(); + } + + // Get the backing Setting, which may be null (if for example it was missing from the file) + IntSetting setting = scSetting.setSelectedValue(value); + if (setting != null) { + mView.putSetting(setting); + } + + closeDialog(); + } else if (mClickedItem instanceof PremiumSingleChoiceSetting) { + PremiumSingleChoiceSetting scSetting = (PremiumSingleChoiceSetting) mClickedItem; + scSetting.setSelectedValue(getValueForSingleChoiceSelection(scSetting, which)); + closeDialog(); + } else if (mClickedItem instanceof StringSingleChoiceSetting) { + StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem; + String value = scSetting.getValueAt(which); + if (!scSetting.getSelectedValue().equals(value)) + mView.onSettingChanged(); + + StringSetting setting = scSetting.setSelectedValue(value); + if (setting != null) { + mView.putSetting(setting); + } + + closeDialog(); + } else if (mClickedItem instanceof SliderSetting) { + SliderSetting sliderSetting = (SliderSetting) mClickedItem; + if (sliderSetting.getSelectedValue() != mSeekbarProgress) { + mView.onSettingChanged(); + } + + if (sliderSetting.getSetting() instanceof FloatSetting) { + float value = (float) mSeekbarProgress; + + FloatSetting setting = sliderSetting.setSelectedValue(value); + if (setting != null) { + mView.putSetting(setting); + } + } else { + IntSetting setting = sliderSetting.setSelectedValue(mSeekbarProgress); + if (setting != null) { + mView.putSetting(setting); + } + } + + closeDialog(); + } + + mClickedItem = null; + mSeekbarProgress = -1; + } + + public void closeDialog() { + if (mDialog != null) { + if (mClickedPosition != -1) { + notifyItemChanged(mClickedPosition); + mClickedPosition = -1; + } + mDialog.dismiss(); + mDialog = null; + } + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + mSeekbarProgress = progress; + mTextSliderValue.setText(String.valueOf(mSeekbarProgress)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) { + int valuesId = item.getValuesId(); + + if (valuesId > 0) { + int[] valuesArray = mContext.getResources().getIntArray(valuesId); + return valuesArray[which]; + } else { + return which; + } + } + + private int getValueForSingleChoiceSelection(PremiumSingleChoiceSetting item, int which) { + int valuesId = item.getValuesId(); + + if (valuesId > 0) { + int[] valuesArray = mContext.getResources().getIntArray(valuesId); + return valuesArray[which]; + } else { + return which; + } + } + + private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) { + int value = item.getSelectedValue(); + int valuesId = item.getValuesId(); + + if (valuesId > 0) { + int[] valuesArray = mContext.getResources().getIntArray(valuesId); + for (int index = 0; index < valuesArray.length; index++) { + int current = valuesArray[index]; + if (current == value) { + return index; + } + } + } else { + return value; + } + + return -1; + } + + private int getSelectionForSingleChoiceValue(PremiumSingleChoiceSetting item) { + int value = item.getSelectedValue(); + int valuesId = item.getValuesId(); + + if (valuesId > 0) { + int[] valuesArray = mContext.getResources().getIntArray(valuesId); + for (int index = 0; index < valuesArray.length; index++) { + int current = valuesArray[index]; + if (current == value) { + return index; + } + } + } else { + return value; + } + + return -1; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java new file mode 100644 index 000000000..5799dcb8d --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java @@ -0,0 +1,136 @@ +package org.citra.citra_emu.features.settings.ui; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.Setting; +import org.citra.citra_emu.features.settings.model.Settings; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.ui.DividerItemDecoration; + +import java.util.ArrayList; + +public final class SettingsFragment extends Fragment implements SettingsFragmentView { + private static final String ARGUMENT_MENU_TAG = "menu_tag"; + private static final String ARGUMENT_GAME_ID = "game_id"; + + private SettingsFragmentPresenter mPresenter = new SettingsFragmentPresenter(this); + private SettingsActivityView mActivity; + + private SettingsAdapter mAdapter; + + public static Fragment newInstance(String menuTag, String gameId) { + SettingsFragment fragment = new SettingsFragment(); + + Bundle arguments = new Bundle(); + arguments.putString(ARGUMENT_MENU_TAG, menuTag); + arguments.putString(ARGUMENT_GAME_ID, gameId); + + fragment.setArguments(arguments); + return fragment; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + mActivity = (SettingsActivityView) context; + mPresenter.onAttach(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setRetainInstance(true); + String menuTag = getArguments().getString(ARGUMENT_MENU_TAG); + String gameId = getArguments().getString(ARGUMENT_GAME_ID); + + mAdapter = new SettingsAdapter(this, getActivity()); + + mPresenter.onCreate(menuTag, gameId); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_settings, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + LinearLayoutManager manager = new LinearLayoutManager(getActivity()); + + RecyclerView recyclerView = view.findViewById(R.id.list_settings); + + recyclerView.setAdapter(mAdapter); + recyclerView.setLayoutManager(manager); + recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null)); + + SettingsActivityView activity = (SettingsActivityView) getActivity(); + + mPresenter.onViewCreated(activity.getSettings()); + } + + @Override + public void onDetach() { + super.onDetach(); + mActivity = null; + + if (mAdapter != null) { + mAdapter.closeDialog(); + } + } + + @Override + public void onSettingsFileLoaded(Settings settings) { + mPresenter.setSettings(settings); + } + + @Override + public void passSettingsToActivity(Settings settings) { + if (mActivity != null) { + mActivity.setSettings(settings); + } + } + + @Override + public void showSettingsList(ArrayList<SettingsItem> settingsList) { + mAdapter.setSettings(settingsList); + } + + @Override + public void loadDefaultSettings() { + mPresenter.loadDefaultSettings(); + } + + @Override + public void loadSubMenu(String menuKey) { + mActivity.showSettingsFragment(menuKey, true, getArguments().getString(ARGUMENT_GAME_ID)); + } + + @Override + public void showToastMessage(String message, boolean is_long) { + mActivity.showToastMessage(message, is_long); + } + + @Override + public void putSetting(Setting setting) { + mPresenter.putSetting(setting); + } + + @Override + public void onSettingChanged() { + mActivity.onSettingChanged(); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java new file mode 100644 index 000000000..f845e9ce8 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java @@ -0,0 +1,411 @@ +package org.citra.citra_emu.features.settings.ui; + +import android.app.Activity; +import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.text.TextUtils; + +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.Setting; +import org.citra.citra_emu.features.settings.model.SettingSection; +import org.citra.citra_emu.features.settings.model.Settings; +import org.citra.citra_emu.features.settings.model.StringSetting; +import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; +import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; +import org.citra.citra_emu.features.settings.model.view.HeaderSetting; +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; +import org.citra.citra_emu.features.settings.model.view.PremiumHeader; +import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; +import org.citra.citra_emu.features.settings.model.view.SliderSetting; +import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; +import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; +import org.citra.citra_emu.features.settings.utils.SettingsFile; +import org.citra.citra_emu.utils.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; + +public final class SettingsFragmentPresenter { + private SettingsFragmentView mView; + + private String mMenuTag; + private String mGameID; + + private Settings mSettings; + private ArrayList<SettingsItem> mSettingsList; + + public SettingsFragmentPresenter(SettingsFragmentView view) { + mView = view; + } + + public void onCreate(String menuTag, String gameId) { + mGameID = gameId; + mMenuTag = menuTag; + } + + public void onViewCreated(Settings settings) { + setSettings(settings); + } + + /** + * If the screen is rotated, the Activity will forget the settings map. This fragment + * won't, though; so rather than have the Activity reload from disk, have the fragment pass + * the settings map back to the Activity. + */ + public void onAttach() { + if (mSettings != null) { + mView.passSettingsToActivity(mSettings); + } + } + + public void putSetting(Setting setting) { + mSettings.getSection(setting.getSection()).putSetting(setting); + } + + private StringSetting asStringSetting(Setting setting) { + if (setting == null) { + return null; + } + + StringSetting stringSetting = new StringSetting(setting.getKey(), setting.getSection(), setting.getValueAsString()); + putSetting(stringSetting); + return stringSetting; + } + + public void loadDefaultSettings() { + loadSettingsList(); + } + + public void setSettings(Settings settings) { + if (mSettingsList == null && settings != null) { + mSettings = settings; + + loadSettingsList(); + } else { + mView.getActivity().setTitle(R.string.preferences_settings); + mView.showSettingsList(mSettingsList); + } + } + + private void loadSettingsList() { + if (!TextUtils.isEmpty(mGameID)) { + mView.getActivity().setTitle("Game Settings: " + mGameID); + } + ArrayList<SettingsItem> sl = new ArrayList<>(); + + if (mMenuTag == null) { + return; + } + + switch (mMenuTag) { + case SettingsFile.FILE_NAME_CONFIG: + addConfigSettings(sl); + break; + case Settings.SECTION_PREMIUM: + addPremiumSettings(sl); + break; + case Settings.SECTION_CORE: + addGeneralSettings(sl); + break; + case Settings.SECTION_SYSTEM: + addSystemSettings(sl); + break; + case Settings.SECTION_CAMERA: + addCameraSettings(sl); + break; + case Settings.SECTION_CONTROLS: + addInputSettings(sl); + break; + case Settings.SECTION_RENDERER: + addGraphicsSettings(sl); + break; + case Settings.SECTION_AUDIO: + addAudioSettings(sl); + break; + case Settings.SECTION_DEBUG: + addDebugSettings(sl); + break; + default: + mView.showToastMessage("Unimplemented menu", false); + return; + } + + mSettingsList = sl; + mView.showSettingsList(mSettingsList); + } + + private void addConfigSettings(ArrayList<SettingsItem> sl) { + mView.getActivity().setTitle(R.string.preferences_settings); + + sl.add(new SubmenuSetting(null, null, R.string.preferences_premium, 0, Settings.SECTION_PREMIUM)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_controls, 0, Settings.SECTION_CONTROLS)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_graphics, 0, Settings.SECTION_RENDERER)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_audio, 0, Settings.SECTION_AUDIO)); + sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG)); + } + + private void addPremiumSettings(ArrayList<SettingsItem> sl) { + mView.getActivity().setTitle(R.string.preferences_premium); + + SettingSection premiumSection = mSettings.getSection(Settings.SECTION_PREMIUM); + Setting design = premiumSection.getSetting(SettingsFile.KEY_DESIGN); + + sl.add(new PremiumHeader()); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNames, R.array.designValues, 0, design, mView)); + } else { + // Pre-Android 10 does not support System Default + sl.add(new PremiumSingleChoiceSetting(SettingsFile.KEY_DESIGN, Settings.SECTION_PREMIUM, R.string.design, 0, R.array.designNamesOld, R.array.designValuesOld, 0, design, mView)); + } + + String[] textureFilterNames = NativeLibrary.GetTextureFilterNames(); + Setting textureFilterName = premiumSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME); + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME, Settings.SECTION_PREMIUM, R.string.texture_filter_name, R.string.texture_filter_description, textureFilterNames, textureFilterNames, "none", textureFilterName)); + } + + private void addGeneralSettings(ArrayList<SettingsItem> sl) { + mView.getActivity().setTitle(R.string.preferences_general); + + SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); + Setting frameLimitEnable = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED); + Setting frameLimitValue = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT); + + sl.add(new CheckBoxSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED, Settings.SECTION_RENDERER, R.string.frame_limit_enable, R.string.frame_limit_enable_description, true, frameLimitEnable)); + sl.add(new SliderSetting(SettingsFile.KEY_FRAME_LIMIT, Settings.SECTION_RENDERER, R.string.frame_limit_slider, R.string.frame_limit_slider_description, 1, 200, "%", 100, frameLimitValue)); + } + + private void addSystemSettings(ArrayList<SettingsItem> sl) { + mView.getActivity().setTitle(R.string.preferences_system); + + SettingSection systemSection = mSettings.getSection(Settings.SECTION_SYSTEM); + Setting region = systemSection.getSetting(SettingsFile.KEY_REGION_VALUE); + Setting language = systemSection.getSetting(SettingsFile.KEY_LANGUAGE); + Setting systemClock = systemSection.getSetting(SettingsFile.KEY_INIT_CLOCK); + Setting dateTime = systemSection.getSetting(SettingsFile.KEY_INIT_TIME); + + sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, Settings.SECTION_SYSTEM, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, -1, region)); + sl.add(new SingleChoiceSetting(SettingsFile.KEY_LANGUAGE, Settings.SECTION_SYSTEM, R.string.emulated_language, 0, R.array.languageNames, R.array.languageValues, 1, language)); + sl.add(new SingleChoiceSetting(SettingsFile.KEY_INIT_CLOCK, Settings.SECTION_SYSTEM, R.string.init_clock, R.string.init_clock_description, R.array.systemClockNames, R.array.systemClockValues, 0, systemClock)); + sl.add(new DateTimeSetting(SettingsFile.KEY_INIT_TIME, Settings.SECTION_SYSTEM, R.string.init_time, R.string.init_time_description, "2000-01-01 00:00:01", dateTime)); + } + + private void addCameraSettings(ArrayList<SettingsItem> sl) { + final Activity activity = mView.getActivity(); + activity.setTitle(R.string.preferences_camera); + + // Get the camera IDs + CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + ArrayList<String> supportedCameraNameList = new ArrayList<>(); + ArrayList<String> supportedCameraIdList = new ArrayList<>(); + if (cameraManager != null) { + try { + for (String id : cameraManager.getCameraIdList()) { + final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); + if (Objects.requireNonNull(characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { + continue; // Legacy cameras cannot be used with the NDK + } + + supportedCameraIdList.add(id); + + final int facing = Objects.requireNonNull(characteristics.get(CameraCharacteristics.LENS_FACING)); + int stringId = R.string.camera_facing_external; + switch (facing) { + case CameraCharacteristics.LENS_FACING_FRONT: + stringId = R.string.camera_facing_front; + break; + case CameraCharacteristics.LENS_FACING_BACK: + stringId = R.string.camera_facing_back; + break; + case CameraCharacteristics.LENS_FACING_EXTERNAL: + stringId = R.string.camera_facing_external; + break; + } + supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(stringId))); + } + } catch (CameraAccessException e) { + Log.error("Couldn't retrieve camera list"); + e.printStackTrace(); + } + } + + // Create the names and values for display + ArrayList<String> cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames))); + cameraDeviceNameList.addAll(supportedCameraNameList); + ArrayList<String> cameraDeviceValueList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceValues))); + cameraDeviceValueList.addAll(supportedCameraIdList); + + final String[] cameraDeviceNames = cameraDeviceNameList.toArray(new String[]{}); + final String[] cameraDeviceValues = cameraDeviceValueList.toArray(new String[]{}); + + final boolean haveCameraDevices = !supportedCameraIdList.isEmpty(); + + String[] imageSourceNames = activity.getResources().getStringArray(R.array.cameraImageSourceNames); + String[] imageSourceValues = activity.getResources().getStringArray(R.array.cameraImageSourceValues); + if (!haveCameraDevices) { + // Remove the last entry (ndk / Device Camera) + imageSourceNames = Arrays.copyOfRange(imageSourceNames, 0, imageSourceNames.length - 1); + imageSourceValues = Arrays.copyOfRange(imageSourceValues, 0, imageSourceValues.length - 1); + } + + final String defaultImageSource = haveCameraDevices ? "ndk" : "image"; + + SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA); + + Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME); + Setting innerCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG)); + Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP); + sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0)); + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, innerCameraImageSource)); + if (haveCameraDevices) + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_front", innerCameraConfig)); + sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, innerCameraFlip)); + + Setting outerLeftCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME); + Setting outerLeftCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG)); + Setting outerLeftCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP); + sl.add(new HeaderSetting(null, null, R.string.outer_left_camera, 0)); + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerLeftCameraImageSource)); + if (haveCameraDevices) + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerLeftCameraConfig)); + sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerLeftCameraFlip)); + + Setting outerRightCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME); + Setting outerRightCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG)); + Setting outerRightCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP); + sl.add(new HeaderSetting(null, null, R.string.outer_right_camera, 0)); + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerRightCameraImageSource)); + if (haveCameraDevices) + sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerRightCameraConfig)); + sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerRightCameraFlip)); + } + + private void addInputSettings(ArrayList<SettingsItem> sl) { + mView.getActivity().setTitle(R.string.preferences_controls); + + SettingSection controlsSection = mSettings.getSection(Settings.SECTION_CONTROLS); + Setting buttonA = controlsSection.getSetting(SettingsFile.KEY_BUTTON_A); + Setting buttonB = controlsSection.getSetting(SettingsFile.KEY_BUTTON_B); + Setting buttonX = controlsSection.getSetting(SettingsFile.KEY_BUTTON_X); + Setting buttonY = controlsSection.getSetting(SettingsFile.KEY_BUTTON_Y); + Setting buttonSelect = controlsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT); + Setting buttonStart = controlsSection.getSetting(SettingsFile.KEY_BUTTON_START); + Setting circlepadAxisVert = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL); + Setting circlepadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL); + Setting cstickAxisVert = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL); + Setting cstickAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL); + Setting dpadAxisVert = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL); + Setting dpadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL); + // Setting buttonUp = controlsSection.getSetting(SettingsFile.KEY_BUTTON_UP); + // Setting buttonDown = controlsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN); + // Setting buttonLeft = controlsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT); + // Setting buttonRight = controlsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT); + Setting buttonL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_L); + Setting buttonR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_R); + Setting buttonZL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZL); + Setting buttonZR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZR); + + sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_CONTROLS, R.string.button_a, buttonA)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_CONTROLS, R.string.button_b, buttonB)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_CONTROLS, R.string.button_x, buttonX)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_CONTROLS, R.string.button_y, buttonY)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, Settings.SECTION_CONTROLS, R.string.button_select, buttonSelect)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_CONTROLS, R.string.button_start, buttonStart)); + + sl.add(new HeaderSetting(null, null, R.string.controller_circlepad, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, circlepadAxisVert)); + sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, circlepadAxisHoriz)); + + sl.add(new HeaderSetting(null, null, R.string.controller_c, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, cstickAxisVert)); + sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, cstickAxisHoriz)); + + sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, dpadAxisVert)); + sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, dpadAxisHoriz)); + + // TODO(bunnei): Figure out what to do with these. Configuring is functional, but removing for MVP because they are confusing. + // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_CONTROLS, R.string.generic_up, buttonUp)); + // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_CONTROLS, R.string.generic_down, buttonDown)); + // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_CONTROLS, R.string.generic_left, buttonLeft)); + // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_CONTROLS, R.string.generic_right, buttonRight)); + + sl.add(new HeaderSetting(null, null, R.string.controller_triggers, 0)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_CONTROLS, R.string.button_l, buttonL)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_CONTROLS, R.string.button_r, buttonR)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZL, Settings.SECTION_CONTROLS, R.string.button_zl, buttonZL)); + sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZR, Settings.SECTION_CONTROLS, R.string.button_zr, buttonZR)); + } + + private void addGraphicsSettings(ArrayList<SettingsItem> sl) { + mView.getActivity().setTitle(R.string.preferences_graphics); + + SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); + Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR); + Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE); + Setting useAsynchronousGpuEmulation = rendererSection.getSetting(SettingsFile.KEY_USE_ASYNCHRONOUS_GPU_EMULATION); + Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL); + Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D); + Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D); + Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE); + + SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT); + Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE); + Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT); + Setting cardboardYShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT); + + sl.add(new HeaderSetting(null, null, R.string.renderer, 0)); + sl.add(new SliderSetting(SettingsFile.KEY_RESOLUTION_FACTOR, Settings.SECTION_RENDERER, R.string.internal_resolution, R.string.internal_resolution_description, 1, 4, "x", 1, resolutionFactor)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_ASYNCHRONOUS_GPU_EMULATION, Settings.SECTION_RENDERER, R.string.asynchronous_gpu, R.string.asynchronous_gpu_description, true, useAsynchronousGpuEmulation)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, Settings.SECTION_RENDERER, R.string.shaders_accurate_mul, R.string.shaders_accurate_mul_description, false, shadersAccurateMul)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE, Settings.SECTION_RENDERER, R.string.use_disk_shader_cache, R.string.use_disk_shader_cache_description, true, useDiskShaderCache)); + + sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0)); + sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode)); + sl.add(new SliderSetting(SettingsFile.KEY_FACTOR_3D, Settings.SECTION_RENDERER, R.string.factor3d, R.string.factor3d_description, 0, 100, "%", 0, factor3d)); + + sl.add(new HeaderSetting(null, null, R.string.cardboard_vr, 0)); + sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE, Settings.SECTION_LAYOUT, R.string.cardboard_screen_size, R.string.cardboard_screen_size_description, 30, 100, "%", 85, cardboardScreenSize)); + sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_x_shift, R.string.cardboard_x_shift_description, -100, 100, "%", 0, cardboardXShift)); + sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_y_shift, R.string.cardboard_y_shift_description, -100, 100, "%", 0, cardboardYShift)); + } + + private void addAudioSettings(ArrayList<SettingsItem> sl) { + mView.getActivity().setTitle(R.string.preferences_audio); + + SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO); + Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING); + Setting micInputType = audioSection.getSetting(SettingsFile.KEY_MIC_INPUT_TYPE); + + sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, true, audioStretch)); + sl.add(new SingleChoiceSetting(SettingsFile.KEY_MIC_INPUT_TYPE, Settings.SECTION_AUDIO, R.string.audio_input_type, 0, R.array.audioInputTypeNames, R.array.audioInputTypeValues, 1, micInputType)); + } + + private void addDebugSettings(ArrayList<SettingsItem> sl) { + mView.getActivity().setTitle(R.string.preferences_debug); + + SettingSection coreSection = mSettings.getSection(Settings.SECTION_CORE); + SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER); + Setting useCpuJit = coreSection.getSetting(SettingsFile.KEY_CPU_JIT); + Setting hardwareRenderer = rendererSection.getSetting(SettingsFile.KEY_HW_RENDERER); + Setting hardwareShader = rendererSection.getSetting(SettingsFile.KEY_HW_SHADER); + Setting vsyncEnable = rendererSection.getSetting(SettingsFile.KEY_USE_VSYNC); + + sl.add(new HeaderSetting(null, null, R.string.debug_warning, 0)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, Settings.SECTION_CORE, R.string.cpu_jit, R.string.cpu_jit_description, true, useCpuJit, true, mView)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_RENDERER, Settings.SECTION_RENDERER, R.string.hw_renderer, R.string.hw_renderer_description, true, hardwareRenderer, true, mView)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, Settings.SECTION_RENDERER, R.string.hw_shaders, R.string.hw_shaders_description, true, hardwareShader, true, mView)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, Settings.SECTION_RENDERER, R.string.vsync, R.string.vsync_description, true, vsyncEnable)); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java new file mode 100644 index 000000000..c36eb55a7 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java @@ -0,0 +1,78 @@ +package org.citra.citra_emu.features.settings.ui; + +import androidx.fragment.app.FragmentActivity; + +import org.citra.citra_emu.features.settings.model.Setting; +import org.citra.citra_emu.features.settings.model.Settings; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; + +import java.util.ArrayList; + +/** + * Abstraction for a screen showing a list of settings. Instances of + * this type of view will each display a layer of the setting hierarchy. + */ +public interface SettingsFragmentView { + /** + * Called by the containing Activity to notify the Fragment that an + * asynchronous load operation completed. + * + * @param settings The (possibly null) result of the ini load operation. + */ + void onSettingsFileLoaded(Settings settings); + + /** + * Pass a settings HashMap to the containing activity, so that it can + * share the HashMap with other SettingsFragments; useful so that rotations + * do not require an additional load operation. + * + * @param settings An ArrayList containing all the settings HashMaps. + */ + void passSettingsToActivity(Settings settings); + + /** + * Pass an ArrayList to the View so that it can be displayed on screen. + * + * @param settingsList The result of converting the HashMap to an ArrayList + */ + void showSettingsList(ArrayList<SettingsItem> settingsList); + + /** + * Called by the containing Activity when an asynchronous load operation fails. + * Instructs the Fragment to load the settings screen with defaults selected. + */ + void loadDefaultSettings(); + + /** + * @return The Fragment's containing activity. + */ + FragmentActivity getActivity(); + + /** + * Tell the Fragment to tell the containing Activity to show a new + * Fragment containing a submenu of settings. + * + * @param menuKey Identifier for the settings group that should be shown. + */ + void loadSubMenu(String menuKey); + + /** + * Tell the Fragment to tell the containing activity to display a toast message. + * + * @param message Text to be shown in the Toast + * @param is_long Whether this should be a long Toast or short one. + */ + void showToastMessage(String message, boolean is_long); + + /** + * Have the fragment add a setting to the HashMap. + * + * @param setting The (possibly previously missing) new setting. + */ + void putSetting(Setting setting); + + /** + * Have the fragment tell the containing Activity that a setting was modified. + */ + void onSettingChanged(); +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFrameLayout.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFrameLayout.java new file mode 100644 index 000000000..67bde5709 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFrameLayout.java @@ -0,0 +1,48 @@ +package org.citra.citra_emu.features.settings.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * FrameLayout subclass with few Properties added to simplify animations. + * Don't remove the methods appearing as unused, in order not to break the menu animations + */ +public final class SettingsFrameLayout extends FrameLayout { + private float mVisibleness = 1.0f; + + public SettingsFrameLayout(Context context) { + super(context); + } + + public SettingsFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SettingsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public float getYFraction() { + return getY() / getHeight(); + } + + public void setYFraction(float yFraction) { + final int height = getHeight(); + setY((height > 0) ? (yFraction * height) : -9999); + } + + public float getVisibleness() { + return mVisibleness; + } + + public void setVisibleness(float visibleness) { + setScaleX(visibleness); + setScaleY(visibleness); + setAlpha(visibleness); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java new file mode 100644 index 000000000..d914f7d0b --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java @@ -0,0 +1,54 @@ +package org.citra.citra_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.CheckBox; +import android.widget.TextView; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.features.settings.ui.SettingsAdapter; + +public final class CheckBoxSettingViewHolder extends SettingViewHolder { + private CheckBoxSetting mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + private CheckBox mCheckbox; + + public CheckBoxSettingViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + mCheckbox = root.findViewById(R.id.checkbox); + } + + @Override + public void bind(SettingsItem item) { + mItem = (CheckBoxSetting) item; + + mTextSettingName.setText(item.getNameId()); + + if (item.getDescriptionId() > 0) { + mTextSettingDescription.setText(item.getDescriptionId()); + mTextSettingDescription.setVisibility(View.VISIBLE); + } else { + mTextSettingDescription.setText(""); + mTextSettingDescription.setVisibility(View.GONE); + } + + mCheckbox.setChecked(mItem.isChecked()); + } + + @Override + public void onClick(View clicked) { + mCheckbox.toggle(); + + getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked()); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java new file mode 100644 index 000000000..09ea93010 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java @@ -0,0 +1,47 @@ +package org.citra.citra_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.TextView; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.view.DateTimeSetting; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.features.settings.ui.SettingsAdapter; +import org.citra.citra_emu.utils.Log; + +public final class DateTimeViewHolder extends SettingViewHolder { + private DateTimeSetting mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + public DateTimeViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + Log.error("test " + mTextSettingName); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + Log.error("test " + mTextSettingDescription); + } + + @Override + public void bind(SettingsItem item) { + mItem = (DateTimeSetting) item; + mTextSettingName.setText(item.getNameId()); + + if (item.getDescriptionId() > 0) { + mTextSettingDescription.setText(item.getDescriptionId()); + mTextSettingDescription.setVisibility(View.VISIBLE); + } else { + mTextSettingDescription.setVisibility(View.GONE); + } + } + + @Override + public void onClick(View clicked) { + getAdapter().onDateTimeClick(mItem, getAdapterPosition()); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java new file mode 100644 index 000000000..baf80ed76 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java @@ -0,0 +1,32 @@ +package org.citra.citra_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.TextView; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.features.settings.ui.SettingsAdapter; + +public final class HeaderViewHolder extends SettingViewHolder { + private TextView mHeaderName; + + public HeaderViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + itemView.setOnClickListener(null); + } + + @Override + protected void findViews(View root) { + mHeaderName = root.findViewById(R.id.text_header_name); + } + + @Override + public void bind(SettingsItem item) { + mHeaderName.setText(item.getNameId()); + } + + @Override + public void onClick(View clicked) { + // no-op + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java new file mode 100644 index 000000000..7d95c250a --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java @@ -0,0 +1,55 @@ +package org.citra.citra_emu.features.settings.ui.viewholder; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.view.View; +import android.widget.TextView; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.features.settings.ui.SettingsAdapter; + +public final class InputBindingSettingViewHolder extends SettingViewHolder { + private InputBindingSetting mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + private Context mContext; + + public InputBindingSettingViewHolder(View itemView, SettingsAdapter adapter, Context context) { + super(itemView, adapter); + + mContext = context; + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + } + + @Override + public void bind(SettingsItem item) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); + + mItem = (InputBindingSetting) item; + + mTextSettingName.setText(item.getNameId()); + + String key = sharedPreferences.getString(mItem.getKey(), ""); + if (key != null && !key.isEmpty()) { + mTextSettingDescription.setText(key); + mTextSettingDescription.setVisibility(View.VISIBLE); + } else { + mTextSettingDescription.setVisibility(View.GONE); + } + } + + @Override + public void onClick(View clicked) { + getAdapter().onInputBindingClick(mItem, getAdapterPosition()); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/PremiumViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/PremiumViewHolder.java new file mode 100644 index 000000000..be0853ff0 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/PremiumViewHolder.java @@ -0,0 +1,57 @@ +package org.citra.citra_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.TextView; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.features.settings.ui.SettingsAdapter; +import org.citra.citra_emu.features.settings.ui.SettingsFragmentView; +import org.citra.citra_emu.ui.main.MainActivity; + +public final class PremiumViewHolder extends SettingViewHolder { + private TextView mHeaderName; + private TextView mTextDescription; + private SettingsFragmentView mView; + + public PremiumViewHolder(View itemView, SettingsAdapter adapter, SettingsFragmentView view) { + super(itemView, adapter); + mView = view; + itemView.setOnClickListener(this); + } + + @Override + protected void findViews(View root) { + mHeaderName = root.findViewById(R.id.text_setting_name); + mTextDescription = root.findViewById(R.id.text_setting_description); + } + + @Override + public void bind(SettingsItem item) { + updateText(); + } + + @Override + public void onClick(View clicked) { + if (MainActivity.isPremiumActive()) { + return; + } + + // Invoke billing flow if Premium is not already active, then refresh the UI to indicate + // the purchase has completed. + MainActivity.invokePremiumBilling(() -> updateText()); + } + + /** + * Update the text shown to the user, based on whether Premium is active + */ + private void updateText() { + if (MainActivity.isPremiumActive()) { + mHeaderName.setText(R.string.premium_settings_welcome); + mTextDescription.setText(R.string.premium_settings_welcome_description); + } else { + mHeaderName.setText(R.string.premium_settings_upsell); + mTextDescription.setText(R.string.premium_settings_upsell_description); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java new file mode 100644 index 000000000..2643ea121 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java @@ -0,0 +1,49 @@ +package org.citra.citra_emu.features.settings.ui.viewholder; + +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.features.settings.ui.SettingsAdapter; + +public abstract class SettingViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + private SettingsAdapter mAdapter; + + public SettingViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView); + + mAdapter = adapter; + + itemView.setOnClickListener(this); + + findViews(itemView); + } + + protected SettingsAdapter getAdapter() { + return mAdapter; + } + + /** + * Gets handles to all this ViewHolder's child views using their XML-defined identifiers. + * + * @param root The newly inflated top-level view. + */ + protected abstract void findViews(View root); + + /** + * Called by the adapter to set this ViewHolder's child views to display the list item + * it must now represent. + * + * @param item The list item that should be represented by this ViewHolder. + */ + public abstract void bind(SettingsItem item); + + /** + * Called when this ViewHolder's view is clicked on. Implementations should usually pass + * this event up to the adapter. + * + * @param clicked The view that was clicked on. + */ + public abstract void onClick(View clicked); +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java new file mode 100644 index 000000000..a175af9f8 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java @@ -0,0 +1,76 @@ +package org.citra.citra_emu.features.settings.ui.viewholder; + +import android.content.res.Resources; +import android.view.View; +import android.widget.TextView; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.view.PremiumSingleChoiceSetting; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting; +import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting; +import org.citra.citra_emu.features.settings.ui.SettingsAdapter; + +public final class SingleChoiceViewHolder extends SettingViewHolder { + private SettingsItem mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + public SingleChoiceViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + } + + @Override + public void bind(SettingsItem item) { + mItem = item; + + mTextSettingName.setText(item.getNameId()); + mTextSettingDescription.setVisibility(View.VISIBLE); + if (item.getDescriptionId() > 0) { + mTextSettingDescription.setText(item.getDescriptionId()); + } else if (item instanceof SingleChoiceSetting) { + SingleChoiceSetting setting = (SingleChoiceSetting) item; + int selected = setting.getSelectedValue(); + Resources resMgr = mTextSettingDescription.getContext().getResources(); + String[] choices = resMgr.getStringArray(setting.getChoicesId()); + int[] values = resMgr.getIntArray(setting.getValuesId()); + for (int i = 0; i < values.length; ++i) { + if (values[i] == selected) { + mTextSettingDescription.setText(choices[i]); + } + } + } else if (item instanceof PremiumSingleChoiceSetting) { + PremiumSingleChoiceSetting setting = (PremiumSingleChoiceSetting) item; + int selected = setting.getSelectedValue(); + Resources resMgr = mTextSettingDescription.getContext().getResources(); + String[] choices = resMgr.getStringArray(setting.getChoicesId()); + int[] values = resMgr.getIntArray(setting.getValuesId()); + for (int i = 0; i < values.length; ++i) { + if (values[i] == selected) { + mTextSettingDescription.setText(choices[i]); + } + } + } else { + mTextSettingDescription.setVisibility(View.GONE); + } + } + + @Override + public void onClick(View clicked) { + int position = getAdapterPosition(); + if (mItem instanceof SingleChoiceSetting) { + getAdapter().onSingleChoiceClick((SingleChoiceSetting) mItem, position); + } else if (mItem instanceof PremiumSingleChoiceSetting) { + getAdapter().onSingleChoiceClick((PremiumSingleChoiceSetting) mItem, position); + } else if (mItem instanceof StringSingleChoiceSetting) { + getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting) mItem, position); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java new file mode 100644 index 000000000..ce503bc54 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java @@ -0,0 +1,46 @@ +package org.citra.citra_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.TextView; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.features.settings.model.view.SliderSetting; +import org.citra.citra_emu.features.settings.ui.SettingsAdapter; + +public final class SliderViewHolder extends SettingViewHolder { + private SliderSetting mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + public SliderViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + } + + @Override + public void bind(SettingsItem item) { + mItem = (SliderSetting) item; + + mTextSettingName.setText(item.getNameId()); + + if (item.getDescriptionId() > 0) { + mTextSettingDescription.setText(item.getDescriptionId()); + mTextSettingDescription.setVisibility(View.VISIBLE); + } else { + mTextSettingDescription.setVisibility(View.GONE); + } + } + + @Override + public void onClick(View clicked) { + getAdapter().onSliderClick(mItem, getAdapterPosition()); + } +} + diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java new file mode 100644 index 000000000..cb8c3e92a --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java @@ -0,0 +1,45 @@ +package org.citra.citra_emu.features.settings.ui.viewholder; + +import android.view.View; +import android.widget.TextView; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.view.SettingsItem; +import org.citra.citra_emu.features.settings.model.view.SubmenuSetting; +import org.citra.citra_emu.features.settings.ui.SettingsAdapter; + +public final class SubmenuViewHolder extends SettingViewHolder { + private SubmenuSetting mItem; + + private TextView mTextSettingName; + private TextView mTextSettingDescription; + + public SubmenuViewHolder(View itemView, SettingsAdapter adapter) { + super(itemView, adapter); + } + + @Override + protected void findViews(View root) { + mTextSettingName = root.findViewById(R.id.text_setting_name); + mTextSettingDescription = root.findViewById(R.id.text_setting_description); + } + + @Override + public void bind(SettingsItem item) { + mItem = (SubmenuSetting) item; + + mTextSettingName.setText(item.getNameId()); + + if (item.getDescriptionId() > 0) { + mTextSettingDescription.setText(item.getDescriptionId()); + mTextSettingDescription.setVisibility(View.VISIBLE); + } else { + mTextSettingDescription.setVisibility(View.GONE); + } + } + + @Override + public void onClick(View clicked) { + getAdapter().onSubmenuClick(mItem); + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java new file mode 100644 index 000000000..5403cd7e4 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java @@ -0,0 +1,337 @@ +package org.citra.citra_emu.features.settings.utils; + +import androidx.annotation.NonNull; + +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.FloatSetting; +import org.citra.citra_emu.features.settings.model.IntSetting; +import org.citra.citra_emu.features.settings.model.Setting; +import org.citra.citra_emu.features.settings.model.SettingSection; +import org.citra.citra_emu.features.settings.model.Settings; +import org.citra.citra_emu.features.settings.model.StringSetting; +import org.citra.citra_emu.features.settings.ui.SettingsActivityView; +import org.citra.citra_emu.utils.BiMap; +import org.citra.citra_emu.utils.DirectoryInitialization; +import org.citra.citra_emu.utils.Log; +import org.ini4j.Wini; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Contains static methods for interacting with .ini files in which settings are stored. + */ +public final class SettingsFile { + public static final String FILE_NAME_CONFIG = "config"; + + public static final String KEY_CPU_JIT = "use_cpu_jit"; + + public static final String KEY_DESIGN = "design"; + + public static final String KEY_PREMIUM = "premium"; + + public static final String KEY_HW_RENDERER = "use_hw_renderer"; + public static final String KEY_HW_SHADER = "use_hw_shader"; + public static final String KEY_SHADERS_ACCURATE_MUL = "shaders_accurate_mul"; + public static final String KEY_USE_SHADER_JIT = "use_shader_jit"; + public static final String KEY_USE_DISK_SHADER_CACHE = "use_disk_shader_cache"; + public static final String KEY_USE_VSYNC = "use_vsync_new"; + public static final String KEY_RESOLUTION_FACTOR = "resolution_factor"; + public static final String KEY_FRAME_LIMIT_ENABLED = "use_frame_limit"; + public static final String KEY_FRAME_LIMIT = "frame_limit"; + public static final String KEY_BACKGROUND_RED = "bg_red"; + public static final String KEY_BACKGROUND_BLUE = "bg_blue"; + public static final String KEY_BACKGROUND_GREEN = "bg_green"; + public static final String KEY_RENDER_3D = "render_3d"; + public static final String KEY_FACTOR_3D = "factor_3d"; + public static final String KEY_PP_SHADER_NAME = "pp_shader_name"; + public static final String KEY_FILTER_MODE = "filter_mode"; + public static final String KEY_TEXTURE_FILTER_NAME = "texture_filter_name"; + public static final String KEY_USE_ASYNCHRONOUS_GPU_EMULATION = "use_asynchronous_gpu_emulation"; + + public static final String KEY_LAYOUT_OPTION = "layout_option"; + public static final String KEY_SWAP_SCREEN = "swap_screen"; + public static final String KEY_CARDBOARD_SCREEN_SIZE = "cardboard_screen_size"; + public static final String KEY_CARDBOARD_X_SHIFT = "cardboard_x_shift"; + public static final String KEY_CARDBOARD_Y_SHIFT = "cardboard_y_shift"; + + public static final String KEY_AUDIO_OUTPUT_ENGINE = "output_engine"; + public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching"; + public static final String KEY_VOLUME = "volume"; + public static final String KEY_MIC_INPUT_TYPE = "mic_input_type"; + + public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd"; + + public static final String KEY_IS_NEW_3DS = "is_new_3ds"; + public static final String KEY_REGION_VALUE = "region_value"; + public static final String KEY_LANGUAGE = "language"; + + public static final String KEY_INIT_CLOCK = "init_clock"; + public static final String KEY_INIT_TIME = "init_time"; + + public static final String KEY_BUTTON_A = "button_a"; + public static final String KEY_BUTTON_B = "button_b"; + public static final String KEY_BUTTON_X = "button_x"; + public static final String KEY_BUTTON_Y = "button_y"; + public static final String KEY_BUTTON_SELECT = "button_select"; + public static final String KEY_BUTTON_START = "button_start"; + public static final String KEY_BUTTON_UP = "button_up"; + public static final String KEY_BUTTON_DOWN = "button_down"; + public static final String KEY_BUTTON_LEFT = "button_left"; + public static final String KEY_BUTTON_RIGHT = "button_right"; + public static final String KEY_BUTTON_L = "button_l"; + public static final String KEY_BUTTON_R = "button_r"; + public static final String KEY_BUTTON_ZL = "button_zl"; + public static final String KEY_BUTTON_ZR = "button_zr"; + public static final String KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical"; + public static final String KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal"; + public static final String KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical"; + public static final String KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal"; + public static final String KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"; + public static final String KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"; + public static final String KEY_CIRCLEPAD_UP = "circlepad_up"; + public static final String KEY_CIRCLEPAD_DOWN = "circlepad_down"; + public static final String KEY_CIRCLEPAD_LEFT = "circlepad_left"; + public static final String KEY_CIRCLEPAD_RIGHT = "circlepad_right"; + public static final String KEY_CSTICK_UP = "cstick_up"; + public static final String KEY_CSTICK_DOWN = "cstick_down"; + public static final String KEY_CSTICK_LEFT = "cstick_left"; + public static final String KEY_CSTICK_RIGHT = "cstick_right"; + + public static final String KEY_CAMERA_OUTER_RIGHT_NAME = "camera_outer_right_name"; + public static final String KEY_CAMERA_OUTER_RIGHT_CONFIG = "camera_outer_right_config"; + public static final String KEY_CAMERA_OUTER_RIGHT_FLIP = "camera_outer_right_flip"; + public static final String KEY_CAMERA_OUTER_LEFT_NAME = "camera_outer_left_name"; + public static final String KEY_CAMERA_OUTER_LEFT_CONFIG = "camera_outer_left_config"; + public static final String KEY_CAMERA_OUTER_LEFT_FLIP = "camera_outer_left_flip"; + public static final String KEY_CAMERA_INNER_NAME = "camera_inner_name"; + public static final String KEY_CAMERA_INNER_CONFIG = "camera_inner_config"; + public static final String KEY_CAMERA_INNER_FLIP = "camera_inner_flip"; + + public static final String KEY_LOG_FILTER = "log_filter"; + + private static BiMap<String, String> sectionsMap = new BiMap<>(); + + static { + //TODO: Add members to sectionsMap when game-specific settings are added + } + + + private SettingsFile() { + } + + /** + * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves + * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it + * failed. + * + * @param ini The ini file to load the settings from + * @param isCustomGame + * @param view The current view. + * @return An Observable that emits a HashMap of the file's contents, then completes. + */ + static HashMap<String, SettingSection> readFile(final File ini, boolean isCustomGame, SettingsActivityView view) { + HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap(); + + BufferedReader reader = null; + + try { + reader = new BufferedReader(new FileReader(ini)); + + SettingSection current = null; + for (String line; (line = reader.readLine()) != null; ) { + if (line.startsWith("[") && line.endsWith("]")) { + current = sectionFromLine(line, isCustomGame); + sections.put(current.getName(), current); + } else if ((current != null)) { + Setting setting = settingFromLine(current, line); + if (setting != null) { + current.putSetting(setting); + } + } + } + } catch (FileNotFoundException e) { + Log.error("[SettingsFile] File not found: " + ini.getAbsolutePath() + e.getMessage()); + if (view != null) + view.onSettingsFileNotFound(); + } catch (IOException e) { + Log.error("[SettingsFile] Error reading from: " + ini.getAbsolutePath() + e.getMessage()); + if (view != null) + view.onSettingsFileNotFound(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + Log.error("[SettingsFile] Error closing: " + ini.getAbsolutePath() + e.getMessage()); + } + } + } + + return sections; + } + + public static HashMap<String, SettingSection> readFile(final String fileName, SettingsActivityView view) { + return readFile(getSettingsFile(fileName), false, view); + } + + /** + * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves + * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it + * failed. + * + * @param gameId the id of the game to load it's settings. + * @param view The current view. + */ + public static HashMap<String, SettingSection> readCustomGameSettings(final String gameId, SettingsActivityView view) { + return readFile(getCustomGameSettingsFile(gameId), true, view); + } + + /** + * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error + * telling why it failed. + * + * @param fileName The target filename without a path or extension. + * @param sections The HashMap containing the Settings we want to serialize. + * @param view The current view. + */ + public static void saveFile(final String fileName, TreeMap<String, SettingSection> sections, + SettingsActivityView view) { + File ini = getSettingsFile(fileName); + + try { + Wini writer = new Wini(ini); + + Set<String> keySet = sections.keySet(); + for (String key : keySet) { + SettingSection section = sections.get(key); + writeSection(writer, section); + } + writer.store(); + } catch (IOException e) { + Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage()); + view.showToastMessage(CitraApplication.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false); + } + } + + + public static void saveCustomGameSettings(final String gameId, final HashMap<String, SettingSection> sections) { + Set<String> sortedSections = new TreeSet<>(sections.keySet()); + + for (String sectionKey : sortedSections) { + SettingSection section = sections.get(sectionKey); + + HashMap<String, Setting> settings = section.getSettings(); + Set<String> sortedKeySet = new TreeSet<>(settings.keySet()); + + for (String settingKey : sortedKeySet) { + Setting setting = settings.get(settingKey); + NativeLibrary.SetUserSetting(gameId, mapSectionNameFromIni(section.getName()), setting.getKey(), setting.getValueAsString()); + } + } + } + + private static String mapSectionNameFromIni(String generalSectionName) { + if (sectionsMap.getForward(generalSectionName) != null) { + return sectionsMap.getForward(generalSectionName); + } + + return generalSectionName; + } + + private static String mapSectionNameToIni(String generalSectionName) { + if (sectionsMap.getBackward(generalSectionName) != null) { + return sectionsMap.getBackward(generalSectionName); + } + + return generalSectionName; + } + + @NonNull + private static File getSettingsFile(String fileName) { + return new File( + DirectoryInitialization.getUserDirectory() + "/config/" + fileName + ".ini"); + } + + private static File getCustomGameSettingsFile(String gameId) { + return new File(DirectoryInitialization.getUserDirectory() + "/GameSettings/" + gameId + ".ini"); + } + + private static SettingSection sectionFromLine(String line, boolean isCustomGame) { + String sectionName = line.substring(1, line.length() - 1); + if (isCustomGame) { + sectionName = mapSectionNameToIni(sectionName); + } + return new SettingSection(sectionName); + } + + /** + * For a line of text, determines what type of data is being represented, and returns + * a Setting object containing this data. + * + * @param current The section currently being parsed by the consuming method. + * @param line The line of text being parsed. + * @return A typed Setting containing the key/value contained in the line. + */ + private static Setting settingFromLine(SettingSection current, String line) { + String[] splitLine = line.split("="); + + if (splitLine.length != 2) { + Log.warning("Skipping invalid config line \"" + line + "\""); + return null; + } + + String key = splitLine[0].trim(); + String value = splitLine[1].trim(); + + if (value.isEmpty()) { + Log.warning("Skipping null value in config line \"" + line + "\""); + return null; + } + + try { + int valueAsInt = Integer.parseInt(value); + + return new IntSetting(key, current.getName(), valueAsInt); + } catch (NumberFormatException ex) { + } + + try { + float valueAsFloat = Float.parseFloat(value); + + return new FloatSetting(key, current.getName(), valueAsFloat); + } catch (NumberFormatException ex) { + } + + return new StringSetting(key, current.getName(), value); + } + + /** + * Writes the contents of a Section HashMap to disk. + * + * @param parser A Wini pointed at a file on disk. + * @param section A section containing settings to be written to the file. + */ + private static void writeSection(Wini parser, SettingSection section) { + // Write the section header. + String header = section.getName(); + + // Write this section's values. + HashMap<String, Setting> settings = section.getSettings(); + Set<String> keySet = settings.keySet(); + + for (String key : keySet) { + Setting setting = settings.get(key); + parser.put(header, setting.getKey(), setting.getValueAsString()); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java new file mode 100644 index 000000000..c18ecd4c3 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/CustomFilePickerFragment.java @@ -0,0 +1,120 @@ +package org.citra.citra_emu.fragments; + +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.FileProvider; + +import com.nononsenseapps.filepicker.FilePickerFragment; + +import org.citra.citra_emu.R; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class CustomFilePickerFragment extends FilePickerFragment { + private static String ALL_FILES = "*"; + private int mTitle; + private static List<String> extensions = Collections.singletonList(ALL_FILES); + + @NonNull + @Override + public Uri toUri(@NonNull final File file) { + return FileProvider + .getUriForFile(getContext(), + getContext().getApplicationContext().getPackageName() + ".filesprovider", + file); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (mode == MODE_DIR) { + TextView ok = getActivity().findViewById(R.id.nnf_button_ok); + ok.setText(R.string.select_dir); + + TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel); + cancel.setVisibility(View.GONE); + } + } + + @Override + protected View inflateRootView(LayoutInflater inflater, ViewGroup container) { + View view = super.inflateRootView(inflater, container); + if (mTitle != 0) { + Toolbar toolbar = view.findViewById(com.nononsenseapps.filepicker.R.id.nnf_picker_toolbar); + ViewGroup parent = (ViewGroup) toolbar.getParent(); + int index = parent.indexOfChild(toolbar); + View newToolbar = inflater.inflate(R.layout.filepicker_toolbar, toolbar, false); + TextView title = newToolbar.findViewById(R.id.filepicker_title); + title.setText(mTitle); + parent.removeView(toolbar); + parent.addView(newToolbar, index); + } + return view; + } + + public void setTitle(int title) { + mTitle = title; + } + + public void setAllowedExtensions(String allowedExtensions) { + if (allowedExtensions == null) + return; + + extensions = Arrays.asList(allowedExtensions.split(",")); + } + + @Override + protected boolean isItemVisible(@NonNull final File file) { + // Some users jump to the conclusion that Dolphin isn't able to detect their + // files if the files don't show up in the file picker when mode == MODE_DIR. + // To avoid this, show files even when the user needs to select a directory. + return (showHiddenItems || !file.isHidden()) && + (file.isDirectory() || extensions.contains(ALL_FILES) || + extensions.contains(fileExtension(file.getName()).toLowerCase())); + } + + @Override + public boolean isCheckable(@NonNull final File file) { + // We need to make a small correction to the isCheckable logic due to + // overriding isItemVisible to show files when mode == MODE_DIR. + // AbstractFilePickerFragment always treats files as checkable when + // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR. + return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile()); + } + + @Override + public void goUp() { + if (Environment.getExternalStorageDirectory().getPath().equals(mCurrentPath.getPath())) { + goToDir(new File("/storage/")); + return; + } + if (mCurrentPath.equals(new File("/storage/"))){ + return; + } + super.goUp(); + } + + @Override + public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) { + if(viewHolder.file.equals(new File("/storage/emulated/"))) + viewHolder.file = new File("/storage/emulated/0/"); + super.onClickDir(view, viewHolder); + } + + private static String fileExtension(@NonNull String filename) { + int i = filename.lastIndexOf('.'); + return i < 0 ? "" : filename.substring(i + 1); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java new file mode 100644 index 000000000..445faa047 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.java @@ -0,0 +1,378 @@ +package org.citra.citra_emu.fragments; + +import android.content.Context; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.view.Choreographer; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.R; +import org.citra.citra_emu.activities.EmulationActivity; +import org.citra.citra_emu.overlay.InputOverlay; +import org.citra.citra_emu.utils.DirectoryInitialization; +import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState; +import org.citra.citra_emu.utils.DirectoryStateReceiver; +import org.citra.citra_emu.utils.EmulationMenuSettings; +import org.citra.citra_emu.utils.Log; + +public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback, Choreographer.FrameCallback { + private static final String KEY_GAMEPATH = "gamepath"; + + private static final Handler perfStatsUpdateHandler = new Handler(); + + private SharedPreferences mPreferences; + + private InputOverlay mInputOverlay; + + private EmulationState mEmulationState; + + private DirectoryStateReceiver directoryStateReceiver; + + private EmulationActivity activity; + + private TextView mPerfStats; + + private Runnable perfStatsUpdater; + + public static EmulationFragment newInstance(String gamePath) { + Bundle args = new Bundle(); + args.putString(KEY_GAMEPATH, gamePath); + + EmulationFragment fragment = new EmulationFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + if (context instanceof EmulationActivity) { + activity = (EmulationActivity) context; + NativeLibrary.setEmulationActivity((EmulationActivity) context); + } else { + throw new IllegalStateException("EmulationFragment must have EmulationActivity parent"); + } + } + + /** + * Initialize anything that doesn't depend on the layout / views in here. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // So this fragment doesn't restart on configuration changes; i.e. rotation. + setRetainInstance(true); + + mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + + String gamePath = getArguments().getString(KEY_GAMEPATH); + mEmulationState = new EmulationState(gamePath); + } + + /** + * Initialize the UI and start emulation in here. + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View contents = inflater.inflate(R.layout.fragment_emulation, container, false); + + SurfaceView surfaceView = contents.findViewById(R.id.surface_emulation); + surfaceView.getHolder().addCallback(this); + + mInputOverlay = contents.findViewById(R.id.surface_input_overlay); + mPerfStats = contents.findViewById(R.id.show_fps_text); + + Button doneButton = contents.findViewById(R.id.done_control_config); + if (doneButton != null) { + doneButton.setOnClickListener(v -> stopConfiguringControls()); + } + + // Show/hide the "Show FPS" overlay + updateShowFpsOverlay(); + + // The new Surface created here will get passed to the native code via onSurfaceChanged. + return contents; + } + + @Override + public void onResume() { + super.onResume(); + Choreographer.getInstance().postFrameCallback(this); + if (DirectoryInitialization.areCitraDirectoriesReady()) { + mEmulationState.run(activity.isActivityRecreated()); + } else { + setupCitraDirectoriesThenStartEmulation(); + } + } + + @Override + public void onPause() { + if (directoryStateReceiver != null) { + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(directoryStateReceiver); + directoryStateReceiver = null; + } + + if (mEmulationState.isRunning()) { + mEmulationState.pause(); + } + + Choreographer.getInstance().removeFrameCallback(this); + super.onPause(); + } + + @Override + public void onDetach() { + NativeLibrary.clearEmulationActivity(); + super.onDetach(); + } + + private void setupCitraDirectoriesThenStartEmulation() { + IntentFilter statusIntentFilter = new IntentFilter( + DirectoryInitialization.BROADCAST_ACTION); + + directoryStateReceiver = + new DirectoryStateReceiver(directoryInitializationState -> + { + if (directoryInitializationState == + DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) { + mEmulationState.run(activity.isActivityRecreated()); + } else if (directoryInitializationState == + DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) { + Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT) + .show(); + } else if (directoryInitializationState == + DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { + Toast.makeText(getContext(), R.string.external_storage_not_mounted, + Toast.LENGTH_SHORT) + .show(); + } + }); + + // Registers the DirectoryStateReceiver and its intent filters + LocalBroadcastManager.getInstance(getActivity()).registerReceiver( + directoryStateReceiver, + statusIntentFilter); + DirectoryInitialization.start(getActivity()); + } + + public void refreshInputOverlay() { + mInputOverlay.refreshControls(); + } + + public void resetInputOverlay() { + // Reset button scale + SharedPreferences.Editor editor = mPreferences.edit(); + editor.putInt("controlScale", 50); + editor.apply(); + + mInputOverlay.resetButtonPlacement(); + } + + public void updateShowFpsOverlay() { + if (EmulationMenuSettings.getShowFps()) { + final int SYSTEM_FPS = 0; + final int FPS = 1; + final int FRAMETIME = 2; + final int SPEED = 3; + + perfStatsUpdater = () -> + { + final double[] perfStats = NativeLibrary.GetPerfStats(); + if (perfStats[FPS] > 0) { + mPerfStats.setText(String.format("FPS: %d Speed: %d%%", (int) (perfStats[FPS] + 0.5), + (int) (perfStats[SPEED] * 100.0 + 0.5))); + } + + perfStatsUpdateHandler.postDelayed(perfStatsUpdater, 3000); + }; + perfStatsUpdateHandler.post(perfStatsUpdater); + + mPerfStats.setVisibility(View.VISIBLE); + } else { + if (perfStatsUpdater != null) { + perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater); + } + + mPerfStats.setVisibility(View.GONE); + } + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + // We purposely don't do anything here. + // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height); + mEmulationState.newSurface(holder.getSurface()); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mEmulationState.clearSurface(); + } + + @Override + public void doFrame(long frameTimeNanos) { + Choreographer.getInstance().postFrameCallback(this); + NativeLibrary.DoFrame(); + } + + public void stopEmulation() { + mEmulationState.stop(); + } + + public void startConfiguringControls() { + getView().findViewById(R.id.done_control_config).setVisibility(View.VISIBLE); + mInputOverlay.setIsInEditMode(true); + } + + public void stopConfiguringControls() { + getView().findViewById(R.id.done_control_config).setVisibility(View.GONE); + mInputOverlay.setIsInEditMode(false); + } + + public boolean isConfiguringControls() { + return mInputOverlay.isInEditMode(); + } + + private static class EmulationState { + private final String mGamePath; + private State state; + private Surface mSurface; + private boolean mRunWhenSurfaceIsValid; + + EmulationState(String gamePath) { + mGamePath = gamePath; + // Starting state is stopped. + state = State.STOPPED; + } + + public synchronized boolean isStopped() { + return state == State.STOPPED; + } + + // Getters for the current state + + public synchronized boolean isPaused() { + return state == State.PAUSED; + } + + public synchronized boolean isRunning() { + return state == State.RUNNING; + } + + public synchronized void stop() { + if (state != State.STOPPED) { + Log.debug("[EmulationFragment] Stopping emulation."); + state = State.STOPPED; + NativeLibrary.StopEmulation(); + } else { + Log.warning("[EmulationFragment] Stop called while already stopped."); + } + } + + // State changing methods + + public synchronized void pause() { + if (state != State.PAUSED) { + state = State.PAUSED; + Log.debug("[EmulationFragment] Pausing emulation."); + + // Release the surface before pausing, since emulation has to be running for that. + NativeLibrary.SurfaceDestroyed(); + NativeLibrary.PauseEmulation(); + } else { + Log.warning("[EmulationFragment] Pause called while already paused."); + } + } + + public synchronized void run(boolean isActivityRecreated) { + if (isActivityRecreated) { + if (NativeLibrary.IsRunning()) { + state = State.PAUSED; + } + } else { + Log.debug("[EmulationFragment] activity resumed or fresh start"); + } + + // If the surface is set, run now. Otherwise, wait for it to get set. + if (mSurface != null) { + runWithValidSurface(); + } else { + mRunWhenSurfaceIsValid = true; + } + } + + // Surface callbacks + public synchronized void newSurface(Surface surface) { + mSurface = surface; + if (mRunWhenSurfaceIsValid) { + runWithValidSurface(); + } + } + + public synchronized void clearSurface() { + if (mSurface == null) { + Log.warning("[EmulationFragment] clearSurface called, but surface already null."); + } else { + mSurface = null; + Log.debug("[EmulationFragment] Surface destroyed."); + + if (state == State.RUNNING) { + NativeLibrary.SurfaceDestroyed(); + state = State.PAUSED; + } else if (state == State.PAUSED) { + Log.warning("[EmulationFragment] Surface cleared while emulation paused."); + } else { + Log.warning("[EmulationFragment] Surface cleared while emulation stopped."); + } + } + } + + private void runWithValidSurface() { + mRunWhenSurfaceIsValid = false; + if (state == State.STOPPED) { + NativeLibrary.SurfaceChanged(mSurface); + Thread mEmulationThread = new Thread(() -> + { + Log.debug("[EmulationFragment] Starting emulation thread."); + NativeLibrary.Run(mGamePath); + }, "NativeEmulation"); + mEmulationThread.start(); + + } else if (state == State.PAUSED) { + Log.debug("[EmulationFragment] Resuming emulation."); + NativeLibrary.SurfaceChanged(mSurface); + NativeLibrary.UnPauseEmulation(); + } else { + Log.debug("[EmulationFragment] Bug, run called while already running."); + } + state = State.RUNNING; + } + + private enum State { + STOPPED, RUNNING, PAUSED + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java new file mode 100644 index 000000000..a4ffc59c7 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.java @@ -0,0 +1,76 @@ +package org.citra.citra_emu.model; + +import android.content.ContentValues; +import android.database.Cursor; + +import java.nio.file.Paths; + +public final class Game { + private String mTitle; + private String mDescription; + private String mPath; + private String mGameId; + private String mCompany; + private String mRegions; + + public Game(String title, String description, String regions, String path, + String gameId, String company) { + mTitle = title; + mDescription = description; + mRegions = regions; + mPath = path; + mGameId = gameId; + mCompany = company; + } + + public static ContentValues asContentValues(String title, String description, String regions, String path, String gameId, String company) { + ContentValues values = new ContentValues(); + + if (gameId.isEmpty()) { + // Homebrew, etc. may not have a game ID, use filename as a unique identifier + gameId = Paths.get(path).getFileName().toString(); + } + + values.put(GameDatabase.KEY_GAME_TITLE, title); + values.put(GameDatabase.KEY_GAME_DESCRIPTION, description); + values.put(GameDatabase.KEY_GAME_REGIONS, regions); + values.put(GameDatabase.KEY_GAME_PATH, path); + values.put(GameDatabase.KEY_GAME_ID, gameId); + values.put(GameDatabase.KEY_GAME_COMPANY, company); + + return values; + } + + public static Game fromCursor(Cursor cursor) { + return new Game(cursor.getString(GameDatabase.GAME_COLUMN_TITLE), + cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION), + cursor.getString(GameDatabase.GAME_COLUMN_REGIONS), + cursor.getString(GameDatabase.GAME_COLUMN_PATH), + cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID), + cursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); + } + + public String getTitle() { + return mTitle; + } + + public String getDescription() { + return mDescription; + } + + public String getCompany() { + return mCompany; + } + + public String getRegions() { + return mRegions; + } + + public String getPath() { + return mPath; + } + + public String getGameId() { + return mGameId; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java b/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java new file mode 100644 index 000000000..215528541 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/GameDatabase.java @@ -0,0 +1,280 @@ +package org.citra.citra_emu.model; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.utils.Log; + +import java.io.File; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import rx.Observable; + +/** + * A helper class that provides several utilities simplifying interaction with + * the SQLite database. + */ +public final class GameDatabase extends SQLiteOpenHelper { + public static final int COLUMN_DB_ID = 0; + public static final int GAME_COLUMN_PATH = 1; + public static final int GAME_COLUMN_TITLE = 2; + public static final int GAME_COLUMN_DESCRIPTION = 3; + public static final int GAME_COLUMN_REGIONS = 4; + public static final int GAME_COLUMN_GAME_ID = 5; + public static final int GAME_COLUMN_COMPANY = 6; + public static final int FOLDER_COLUMN_PATH = 1; + public static final String KEY_DB_ID = "_id"; + public static final String KEY_GAME_PATH = "path"; + public static final String KEY_GAME_TITLE = "title"; + public static final String KEY_GAME_DESCRIPTION = "description"; + public static final String KEY_GAME_REGIONS = "regions"; + public static final String KEY_GAME_ID = "game_id"; + public static final String KEY_GAME_COMPANY = "company"; + public static final String KEY_FOLDER_PATH = "path"; + public static final String TABLE_NAME_FOLDERS = "folders"; + public static final String TABLE_NAME_GAMES = "games"; + private static final int DB_VERSION = 2; + private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY"; + private static final String TYPE_INTEGER = " INTEGER"; + private static final String TYPE_STRING = " TEXT"; + + private static final String CONSTRAINT_UNIQUE = " UNIQUE"; + + private static final String SEPARATOR = ", "; + + private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "(" + + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR + + KEY_GAME_PATH + TYPE_STRING + SEPARATOR + + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR + + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR + + KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR + + KEY_GAME_ID + TYPE_STRING + SEPARATOR + + KEY_GAME_COMPANY + TYPE_STRING + ")"; + + private static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "(" + + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR + + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")"; + + private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS; + private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES; + + public GameDatabase(Context context) { + // Superclass constructor builds a database or uses an existing one. + super(context, "games.db", null, DB_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase database) { + Log.debug("[GameDatabase] GameDatabase - Creating database..."); + + execSqlAndLog(database, SQL_CREATE_GAMES); + execSqlAndLog(database, SQL_CREATE_FOLDERS); + } + + @Override + public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion) { + Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases.."); + execSqlAndLog(database, SQL_DELETE_FOLDERS); + execSqlAndLog(database, SQL_CREATE_FOLDERS); + + execSqlAndLog(database, SQL_DELETE_GAMES); + execSqlAndLog(database, SQL_CREATE_GAMES); + } + + @Override + public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { + Log.info("[GameDatabase] Upgrading database from schema version " + oldVersion + " to " + + newVersion); + + // Delete all the games + execSqlAndLog(database, SQL_DELETE_GAMES); + execSqlAndLog(database, SQL_CREATE_GAMES); + } + + public void resetDatabase(SQLiteDatabase database) { + execSqlAndLog(database, SQL_DELETE_FOLDERS); + execSqlAndLog(database, SQL_CREATE_FOLDERS); + + execSqlAndLog(database, SQL_DELETE_GAMES); + execSqlAndLog(database, SQL_CREATE_GAMES); + } + + public void scanLibrary(SQLiteDatabase database) { + // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing. + Cursor fileCursor = database.query(TABLE_NAME_GAMES, + null, // Get all columns. + null, // Get all rows. + null, + null, // No grouping. + null, + null); // Order of games is irrelevant. + + // Possibly overly defensive, but ensures that moveToNext() does not skip a row. + fileCursor.moveToPosition(-1); + + while (fileCursor.moveToNext()) { + String gamePath = fileCursor.getString(GAME_COLUMN_PATH); + File game = new File(gamePath); + + if (!game.exists()) { + Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " + + gamePath); + database.delete(TABLE_NAME_GAMES, + KEY_DB_ID + " = ?", + new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))}); + } + } + + // Get a cursor listing all the folders the user has added to the library. + Cursor folderCursor = database.query(TABLE_NAME_FOLDERS, + null, // Get all columns. + null, // Get all rows. + null, + null, // No grouping. + null, + null); // Order of folders is irrelevant. + + Set<String> allowedExtensions = new HashSet<String>(Arrays.asList( + ".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app", ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz")); + + // Possibly overly defensive, but ensures that moveToNext() does not skip a row. + folderCursor.moveToPosition(-1); + + // Iterate through all results of the DB query (i.e. all folders in the library.) + while (folderCursor.moveToNext()) { + String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH); + + File folder = new File(folderPath); + // If the folder is empty because it no longer exists, remove it from the library. + if (!folder.exists()) { + Log.error( + "[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath); + database.delete(TABLE_NAME_FOLDERS, + KEY_DB_ID + " = ?", + new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))}); + } + + addGamesRecursive(database, folder, allowedExtensions, 3); + } + + fileCursor.close(); + folderCursor.close(); + + Arrays.stream(NativeLibrary.GetInstalledGamePaths()) + .forEach(filePath -> attemptToAddGame(database, filePath)); + + database.close(); + } + + private static void addGamesRecursive(SQLiteDatabase database, File parent, Set<String> allowedExtensions, int depth) { + if (depth <= 0) { + return; + } + + File[] children = parent.listFiles(); + if (children != null) { + for (File file : children) { + if (file.isHidden()) { + continue; + } + + if (file.isDirectory()) { + Set<String> newExtensions = new HashSet<>(Arrays.asList( + ".3ds", ".3dsx", ".elf", ".axf", ".cci", ".cxi", ".app")); + addGamesRecursive(database, file, newExtensions, depth - 1); + } else { + String filePath = file.getPath(); + + int extensionStart = filePath.lastIndexOf('.'); + if (extensionStart > 0) { + String fileExtension = filePath.substring(extensionStart); + + // Check that the file has an extension we care about before trying to read out of it. + if (allowedExtensions.contains(fileExtension.toLowerCase())) { + attemptToAddGame(database, filePath); + } + } + } + } + } + } + + private static void attemptToAddGame(SQLiteDatabase database, String filePath) { + String name = NativeLibrary.GetTitle(filePath); + + // If the game's title field is empty, use the filename. + if (name.isEmpty()) { + name = filePath.substring(filePath.lastIndexOf("/") + 1); + } + + String gameId = NativeLibrary.GetGameId(filePath); + + // If the game's ID field is empty, use the filename without extension. + if (gameId.isEmpty()) { + gameId = filePath.substring(filePath.lastIndexOf("/") + 1, + filePath.lastIndexOf(".")); + } + + ContentValues game = Game.asContentValues(name, + NativeLibrary.GetDescription(filePath).replace("\n", " "), + NativeLibrary.GetRegions(filePath), + filePath, + gameId, + NativeLibrary.GetCompany(filePath)); + + // Try to update an existing game first. + int rowsMatched = database.update(TABLE_NAME_GAMES, // Which table to update. + game, + // The values to fill the row with. + KEY_GAME_ID + " = ?", + // The WHERE clause used to find the right row. + new String[]{game.getAsString( + KEY_GAME_ID)}); // The ? in WHERE clause is replaced with this, + // which is provided as an array because there + // could potentially be more than one argument. + + // If update fails, insert a new game instead. + if (rowsMatched == 0) { + Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE)); + database.insert(TABLE_NAME_GAMES, null, game); + } else { + Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE)); + } + } + + public Observable<Cursor> getGames() { + return Observable.create(subscriber -> + { + Log.info("[GameDatabase] Reading games list..."); + + SQLiteDatabase database = getReadableDatabase(); + Cursor resultCursor = database.query( + TABLE_NAME_GAMES, + null, + null, + null, + null, + null, + KEY_GAME_TITLE + " ASC" + ); + + // Pass the result cursor to the consumer. + subscriber.onNext(resultCursor); + + // Tell the consumer we're done; it will unsubscribe implicitly. + subscriber.onCompleted(); + }); + } + + private void execSqlAndLog(SQLiteDatabase database, String sql) { + Log.verbose("[GameDatabase] Executing SQL: " + sql); + database.execSQL(sql); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/GameProvider.java b/src/android/app/src/main/java/org/citra/citra_emu/model/GameProvider.java new file mode 100644 index 000000000..33b289fc4 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/GameProvider.java @@ -0,0 +1,138 @@ +package org.citra.citra_emu.model; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import org.citra.citra_emu.BuildConfig; +import org.citra.citra_emu.utils.Log; + +/** + * Provides an interface allowing Activities to interact with the SQLite database. + * CRUD methods in this class can be called by Activities using getContentResolver(). + */ +public final class GameProvider extends ContentProvider { + public static final String REFRESH_LIBRARY = "refresh"; + public static final String RESET_LIBRARY = "reset"; + + public static final String AUTHORITY = "content://" + BuildConfig.APPLICATION_ID + ".provider"; + public static final Uri URI_FOLDER = + Uri.parse(AUTHORITY + "/" + GameDatabase.TABLE_NAME_FOLDERS + "/"); + public static final Uri URI_REFRESH = Uri.parse(AUTHORITY + "/" + REFRESH_LIBRARY + "/"); + public static final Uri URI_RESET = Uri.parse(AUTHORITY + "/" + RESET_LIBRARY + "/"); + + public static final String MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.dolphin.folder"; + public static final String MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.dolphin.game"; + + + private GameDatabase mDbHelper; + + @Override + public boolean onCreate() { + Log.info("[GameProvider] Creating Content Provider..."); + + mDbHelper = new GameDatabase(getContext()); + + return true; + } + + @Override + public Cursor query(@NonNull Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + Log.info("[GameProvider] Querying URI: " + uri); + + SQLiteDatabase db = mDbHelper.getReadableDatabase(); + + String table = uri.getLastPathSegment(); + + if (table == null) { + Log.error("[GameProvider] Badly formatted URI: " + uri); + return null; + } + + Cursor cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder); + cursor.setNotificationUri(getContext().getContentResolver(), uri); + + return cursor; + } + + @Override + public String getType(@NonNull Uri uri) { + Log.verbose("[GameProvider] Getting MIME type for URI: " + uri); + String lastSegment = uri.getLastPathSegment(); + + if (lastSegment == null) { + Log.error("[GameProvider] Badly formatted URI: " + uri); + return null; + } + + if (lastSegment.equals(GameDatabase.TABLE_NAME_FOLDERS)) { + return MIME_TYPE_FOLDER; + } else if (lastSegment.equals(GameDatabase.TABLE_NAME_GAMES)) { + return MIME_TYPE_GAME; + } + + Log.error("[GameProvider] Unknown MIME type for URI: " + uri); + return null; + } + + @Override + public Uri insert(@NonNull Uri uri, ContentValues values) { + Log.info("[GameProvider] Inserting row at URI: " + uri); + + SQLiteDatabase database = mDbHelper.getWritableDatabase(); + String table = uri.getLastPathSegment(); + + if (table != null) { + if (table.equals(RESET_LIBRARY)) { + mDbHelper.resetDatabase(database); + return uri; + } + if (table.equals(REFRESH_LIBRARY)) { + Log.info( + "[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents..."); + mDbHelper.scanLibrary(database); + return uri; + } + + long id = database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE); + + // If insertion was successful... + if (id > 0) { + // If we just added a folder, add its contents to the game list. + if (table.equals(GameDatabase.TABLE_NAME_FOLDERS)) { + mDbHelper.scanLibrary(database); + } + + // Notify the UI that its contents should be refreshed. + getContext().getContentResolver().notifyChange(uri, null); + uri = Uri.withAppendedPath(uri, Long.toString(id)); + } else { + Log.error("[GameProvider] Row already exists: " + uri + " id: " + id); + } + } else { + Log.error("[GameProvider] Badly formatted URI: " + uri); + } + + database.close(); + + return uri; + } + + @Override + public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { + Log.error("[GameProvider] Delete operations unsupported. URI: " + uri); + return 0; + } + + @Override + public int update(@NonNull Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + Log.error("[GameProvider] Update operations unsupported. URI: " + uri); + return 0; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java new file mode 100644 index 000000000..cdb2f7666 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java @@ -0,0 +1,878 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.citra.citra_emu.overlay; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.preference.PreferenceManager; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.View; +import android.view.View.OnTouchListener; + +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.NativeLibrary.ButtonState; +import org.citra.citra_emu.NativeLibrary.ButtonType; +import org.citra.citra_emu.R; +import org.citra.citra_emu.utils.EmulationMenuSettings; + +import java.util.HashSet; +import java.util.Set; + +/** + * Draws the interactive input overlay on top of the + * {@link SurfaceView} that is rendering emulation. + */ +public final class InputOverlay extends SurfaceView implements OnTouchListener { + private final Set<InputOverlayDrawableButton> overlayButtons = new HashSet<>(); + private final Set<InputOverlayDrawableDpad> overlayDpads = new HashSet<>(); + private final Set<InputOverlayDrawableJoystick> overlayJoysticks = new HashSet<>(); + + private boolean mIsInEditMode = false; + private InputOverlayDrawableButton mButtonBeingConfigured; + private InputOverlayDrawableDpad mDpadBeingConfigured; + private InputOverlayDrawableJoystick mJoystickBeingConfigured; + + private SharedPreferences mPreferences; + + // Stores the ID of the pointer that interacted with the 3DS touchscreen. + private int mTouchscreenPointerId = -1; + + /** + * Constructor + * + * @param context The current {@link Context}. + * @param attrs {@link AttributeSet} for parsing XML attributes. + */ + public InputOverlay(Context context, AttributeSet attrs) { + super(context, attrs); + + mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + if (!mPreferences.getBoolean("OverlayInit", false)) { + defaultOverlay(); + } + + // Reset 3ds touchscreen pointer ID + mTouchscreenPointerId = -1; + + // Load the controls. + refreshControls(); + + // Set the on touch listener. + setOnTouchListener(this); + + // Force draw + setWillNotDraw(false); + + // Request focus for the overlay so it has priority on presses. + requestFocus(); + } + + /** + * Resizes a {@link Bitmap} by a given scale factor + * + * @param context The current {@link Context} + * @param bitmap The {@link Bitmap} to scale. + * @param scale The scale factor for the bitmap. + * @return The scaled {@link Bitmap} + */ + public static Bitmap resizeBitmap(Context context, Bitmap bitmap, float scale) { + // Determine the button size based on the smaller screen dimension. + // This makes sure the buttons are the same size in both portrait and landscape. + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + int minDimension = Math.min(dm.widthPixels, dm.heightPixels); + + return Bitmap.createScaledBitmap(bitmap, + (int) (minDimension * scale), + (int) (minDimension * scale), + true); + } + + /** + * Initializes an InputOverlayDrawableButton, given by resId, with all of the + * parameters set for it to be properly shown on the InputOverlay. + * <p> + * This works due to the way the X and Y coordinates are stored within + * the {@link SharedPreferences}. + * <p> + * In the input overlay configuration menu, + * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay). + * the X and Y coordinates of the button at the END of its touch event + * (when you remove your finger/stylus from the touchscreen) are then stored + * within a SharedPreferences instance so that those values can be retrieved here. + * <p> + * This has a few benefits over the conventional way of storing the values + * (ie. within the Citra ini file). + * <ul> + * <li>No native calls</li> + * <li>Keeps Android-only values inside the Android environment</li> + * </ul> + * <p> + * Technically no modifications should need to be performed on the returned + * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait + * for Android to call the onDraw method. + * + * @param context The current {@link Context}. + * @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State). + * @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State). + * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents. + * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set. + */ + private static InputOverlayDrawableButton initializeOverlayButton(Context context, + int defaultResId, int pressedResId, int buttonId, String orientation) { + // Resources handle for fetching the initial Drawable resource. + final Resources res = context.getResources(); + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. + final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); + + // Decide scale based on button ID and user preference + float scale; + + switch (buttonId) { + case ButtonType.BUTTON_HOME: + case ButtonType.BUTTON_START: + case ButtonType.BUTTON_SELECT: + scale = 0.08f; + break; + case ButtonType.TRIGGER_L: + case ButtonType.TRIGGER_R: + case ButtonType.BUTTON_ZL: + case ButtonType.BUTTON_ZR: + scale = 0.18f; + break; + default: + scale = 0.11f; + break; + } + + scale *= (sPrefs.getInt("controlScale", 50) + 50); + scale /= 100; + + // Initialize the InputOverlayDrawableButton. + final Bitmap defaultStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); + final Bitmap pressedStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(res, pressedResId), scale); + final InputOverlayDrawableButton overlayDrawable = + new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId); + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + String xKey; + String yKey; + + xKey = buttonId + orientation + "-X"; + yKey = buttonId + orientation + "-Y"; + + int drawableX = (int) sPrefs.getFloat(xKey, 0f); + int drawableY = (int) sPrefs.getFloat(yKey, 0f); + + int width = overlayDrawable.getWidth(); + int height = overlayDrawable.getHeight(); + + // Now set the bounds for the InputOverlayDrawableButton. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. + overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); + + // Need to set the image's position + overlayDrawable.setPosition(drawableX, drawableY); + + return overlayDrawable; + } + + /** + * Initializes an {@link InputOverlayDrawableDpad} + * + * @param context The current {@link Context}. + * @param defaultResId The {@link Bitmap} resource ID of the default sate. + * @param pressedOneDirectionResId The {@link Bitmap} resource ID of the pressed sate in one direction. + * @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions. + * @param buttonUp Identifier for the up button. + * @param buttonDown Identifier for the down button. + * @param buttonLeft Identifier for the left button. + * @param buttonRight Identifier for the right button. + * @return the initialized {@link InputOverlayDrawableDpad} + */ + private static InputOverlayDrawableDpad initializeOverlayDpad(Context context, + int defaultResId, + int pressedOneDirectionResId, + int pressedTwoDirectionsResId, + int buttonUp, + int buttonDown, + int buttonLeft, + int buttonRight, + String orientation) { + // Resources handle for fetching the initial Drawable resource. + final Resources res = context.getResources(); + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad. + final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); + + // Decide scale based on button ID and user preference + float scale = 0.22f; + + scale *= (sPrefs.getInt("controlScale", 50) + 50); + scale /= 100; + + // Initialize the InputOverlayDrawableDpad. + final Bitmap defaultStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale); + final Bitmap pressedOneDirectionStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(res, pressedOneDirectionResId), + scale); + final Bitmap pressedTwoDirectionsStateBitmap = + resizeBitmap(context, BitmapFactory.decodeResource(res, pressedTwoDirectionsResId), + scale); + final InputOverlayDrawableDpad overlayDrawable = + new InputOverlayDrawableDpad(res, defaultStateBitmap, + pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap, + buttonUp, buttonDown, buttonLeft, buttonRight); + + // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. + // These were set in the input overlay configuration menu. + int drawableX = (int) sPrefs.getFloat(buttonUp + orientation + "-X", 0f); + int drawableY = (int) sPrefs.getFloat(buttonUp + orientation + "-Y", 0f); + + int width = overlayDrawable.getWidth(); + int height = overlayDrawable.getHeight(); + + // Now set the bounds for the InputOverlayDrawableDpad. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be. + overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height); + + // Need to set the image's position + overlayDrawable.setPosition(drawableX, drawableY); + + return overlayDrawable; + } + + /** + * Initializes an {@link InputOverlayDrawableJoystick} + * + * @param context The current {@link Context} + * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds). + * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around). + * @param pressedResInner Resource ID for the pressed inner image of the joystick. + * @param joystick Identifier for which joystick this is. + * @return the initialized {@link InputOverlayDrawableJoystick}. + */ + private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, + int resOuter, int defaultResInner, int pressedResInner, int joystick, String orientation) { + // Resources handle for fetching the initial Drawable resource. + final Resources res = context.getResources(); + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick. + final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); + + // Decide scale based on user preference + float scale = 0.275f; + scale *= (sPrefs.getInt("controlScale", 50) + 50); + scale /= 100; + + // Initialize the InputOverlayDrawableJoystick. + final Bitmap bitmapOuter = + resizeBitmap(context, BitmapFactory.decodeResource(res, resOuter), scale); + final Bitmap bitmapInnerDefault = BitmapFactory.decodeResource(res, defaultResInner); + final Bitmap bitmapInnerPressed = BitmapFactory.decodeResource(res, pressedResInner); + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + int drawableX = (int) sPrefs.getFloat(joystick + orientation + "-X", 0f); + int drawableY = (int) sPrefs.getFloat(joystick + orientation + "-Y", 0f); + + // Decide inner scale based on joystick ID + float outerScale = 1.f; + if (joystick == ButtonType.STICK_C) { + outerScale = 2.f; + } + + // Now set the bounds for the InputOverlayDrawableJoystick. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. + int outerSize = bitmapOuter.getWidth(); + Rect outerRect = new Rect(drawableX, drawableY, drawableX + (int) (outerSize / outerScale), drawableY + (int) (outerSize / outerScale)); + Rect innerRect = new Rect(0, 0, (int) (outerSize / outerScale), (int) (outerSize / outerScale)); + + // Send the drawableId to the joystick so it can be referenced when saving control position. + final InputOverlayDrawableJoystick overlayDrawable + = new InputOverlayDrawableJoystick(res, bitmapOuter, + bitmapInnerDefault, bitmapInnerPressed, + outerRect, innerRect, joystick); + + // Need to set the image's position + overlayDrawable.setPosition(drawableX, drawableY); + + return overlayDrawable; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + for (InputOverlayDrawableButton button : overlayButtons) { + button.draw(canvas); + } + + for (InputOverlayDrawableDpad dpad : overlayDpads) { + dpad.draw(canvas); + } + + for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { + joystick.draw(canvas); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (isInEditMode()) { + return onTouchWhileEditing(event); + } + + int pointerIndex = event.getActionIndex(); + + if (mPreferences.getBoolean("isTouchEnabled", true)) { + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + if (NativeLibrary.onTouchEvent(event.getX(pointerIndex), event.getY(pointerIndex), true)) { + mTouchscreenPointerId = event.getPointerId(pointerIndex); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + if (mTouchscreenPointerId == event.getPointerId(pointerIndex)) { + // We don't really care where the touch has been released. We only care whether it has been + // released or not. + NativeLibrary.onTouchEvent(0, 0, false); + mTouchscreenPointerId = -1; + } + break; + } + + for (int i = 0; i < event.getPointerCount(); i++) { + if (mTouchscreenPointerId == event.getPointerId(i)) { + NativeLibrary.onTouchMoved(event.getX(i), event.getY(i)); + } + } + } + + for (InputOverlayDrawableButton button : overlayButtons) { + // Determine the button state to apply based on the MotionEvent action flag. + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + // If a pointer enters the bounds of a button, press that button. + if (button.getBounds() + .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { + button.setPressedState(true); + button.setTrackId(event.getPointerId(pointerIndex)); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), + ButtonState.PRESSED); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + // If a pointer ends, release the button it was pressing. + if (button.getTrackId() == event.getPointerId(pointerIndex)) { + button.setPressedState(false); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), + ButtonState.RELEASED); + } + break; + } + } + + for (InputOverlayDrawableDpad dpad : overlayDpads) { + // Determine the button state to apply based on the MotionEvent action flag. + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + // If a pointer enters the bounds of a button, press that button. + if (dpad.getBounds() + .contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { + dpad.setTrackId(event.getPointerId(pointerIndex)); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + // If a pointer ends, release the buttons. + if (dpad.getTrackId() == event.getPointerId(pointerIndex)) { + for (int i = 0; i < 4; i++) { + dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT); + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(i), + NativeLibrary.ButtonState.RELEASED); + } + dpad.setTrackId(-1); + } + break; + } + + if (dpad.getTrackId() != -1) { + for (int i = 0; i < event.getPointerCount(); i++) { + if (dpad.getTrackId() == event.getPointerId(i)) { + float touchX = event.getX(i); + float touchY = event.getY(i); + float maxY = dpad.getBounds().bottom; + float maxX = dpad.getBounds().right; + touchX -= dpad.getBounds().centerX(); + maxX -= dpad.getBounds().centerX(); + touchY -= dpad.getBounds().centerY(); + maxY -= dpad.getBounds().centerY(); + final float AxisX = touchX / maxX; + final float AxisY = touchY / maxY; + + boolean up = false; + boolean down = false; + boolean left = false; + boolean right = false; + if (EmulationMenuSettings.getDpadSlideEnable() || + (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN || + (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { + if (AxisY < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0), + NativeLibrary.ButtonState.PRESSED); + up = true; + } else { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(0), + NativeLibrary.ButtonState.RELEASED); + } + if (AxisY > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1), + NativeLibrary.ButtonState.PRESSED); + down = true; + } else { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(1), + NativeLibrary.ButtonState.RELEASED); + } + if (AxisX < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2), + NativeLibrary.ButtonState.PRESSED); + left = true; + } else { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(2), + NativeLibrary.ButtonState.RELEASED); + } + if (AxisX > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE) { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3), + NativeLibrary.ButtonState.PRESSED); + right = true; + } else { + NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getId(3), + NativeLibrary.ButtonState.RELEASED); + } + + // Set state + if (up) { + if (left) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT); + else if (right) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT); + else + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP); + } else if (down) { + if (left) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT); + else if (right) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT); + else + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN); + } else if (left) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT); + } else if (right) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT); + } else { + dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT); + } + } + } + } + } + } + + for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { + joystick.TrackEvent(event); + int axisID = joystick.getId(); + float[] axises = joystick.getAxisValues(); + + NativeLibrary + .onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisID, axises[0], axises[1]); + } + + invalidate(); + + return true; + } + + public boolean onTouchWhileEditing(MotionEvent event) { + int pointerIndex = event.getActionIndex(); + int fingerPositionX = (int) event.getX(pointerIndex); + int fingerPositionY = (int) event.getY(pointerIndex); + + String orientation = + getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? + "-Portrait" : ""; + + // Maybe combine Button and Joystick as subclasses of the same parent? + // Or maybe create an interface like IMoveableHUDControl? + + for (InputOverlayDrawableButton button : overlayButtons) { + // Determine the button state to apply based on the MotionEvent action flag. + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + // If no button is being moved now, remember the currently touched button to move. + if (mButtonBeingConfigured == null && + button.getBounds().contains(fingerPositionX, fingerPositionY)) { + mButtonBeingConfigured = button; + mButtonBeingConfigured.onConfigureTouch(event); + } + break; + case MotionEvent.ACTION_MOVE: + if (mButtonBeingConfigured != null) { + mButtonBeingConfigured.onConfigureTouch(event); + invalidate(); + return true; + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + if (mButtonBeingConfigured == button) { + // Persist button position by saving new place. + saveControlPosition(mButtonBeingConfigured.getId(), + mButtonBeingConfigured.getBounds().left, + mButtonBeingConfigured.getBounds().top, orientation); + mButtonBeingConfigured = null; + } + break; + } + } + + for (InputOverlayDrawableDpad dpad : overlayDpads) { + // Determine the button state to apply based on the MotionEvent action flag. + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + // If no button is being moved now, remember the currently touched button to move. + if (mButtonBeingConfigured == null && + dpad.getBounds().contains(fingerPositionX, fingerPositionY)) { + mDpadBeingConfigured = dpad; + mDpadBeingConfigured.onConfigureTouch(event); + } + break; + case MotionEvent.ACTION_MOVE: + if (mDpadBeingConfigured != null) { + mDpadBeingConfigured.onConfigureTouch(event); + invalidate(); + return true; + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + if (mDpadBeingConfigured == dpad) { + // Persist button position by saving new place. + saveControlPosition(mDpadBeingConfigured.getId(0), + mDpadBeingConfigured.getBounds().left, mDpadBeingConfigured.getBounds().top, + orientation); + mDpadBeingConfigured = null; + } + break; + } + } + + for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + if (mJoystickBeingConfigured == null && + joystick.getBounds().contains(fingerPositionX, fingerPositionY)) { + mJoystickBeingConfigured = joystick; + mJoystickBeingConfigured.onConfigureTouch(event); + } + break; + case MotionEvent.ACTION_MOVE: + if (mJoystickBeingConfigured != null) { + mJoystickBeingConfigured.onConfigureTouch(event); + invalidate(); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + if (mJoystickBeingConfigured != null) { + saveControlPosition(mJoystickBeingConfigured.getId(), + mJoystickBeingConfigured.getBounds().left, + mJoystickBeingConfigured.getBounds().top, orientation); + mJoystickBeingConfigured = null; + } + break; + } + } + + return true; + } + + private void setDpadState(InputOverlayDrawableDpad dpad, boolean up, boolean down, boolean left, + boolean right) { + if (up) { + if (left) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_LEFT); + else if (right) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP_RIGHT); + else + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_UP); + } else if (down) { + if (left) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_LEFT); + else if (right) + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN_RIGHT); + else + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_DOWN); + } else if (left) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_LEFT); + } else if (right) { + dpad.setState(InputOverlayDrawableDpad.STATE_PRESSED_RIGHT); + } + } + + private void addOverlayControls(String orientation) { + if (mPreferences.getBoolean("buttonToggle0", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_a, + R.drawable.button_a_pressed, ButtonType.BUTTON_A, orientation)); + } + if (mPreferences.getBoolean("buttonToggle1", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_b, + R.drawable.button_b_pressed, ButtonType.BUTTON_B, orientation)); + } + if (mPreferences.getBoolean("buttonToggle2", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_x, + R.drawable.button_x_pressed, ButtonType.BUTTON_X, orientation)); + } + if (mPreferences.getBoolean("buttonToggle3", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_y, + R.drawable.button_y_pressed, ButtonType.BUTTON_Y, orientation)); + } + if (mPreferences.getBoolean("buttonToggle4", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l, + R.drawable.button_l_pressed, ButtonType.TRIGGER_L, orientation)); + } + if (mPreferences.getBoolean("buttonToggle5", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r, + R.drawable.button_r_pressed, ButtonType.TRIGGER_R, orientation)); + } + if (mPreferences.getBoolean("buttonToggle6", false)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zl, + R.drawable.button_zl_pressed, ButtonType.BUTTON_ZL, orientation)); + } + if (mPreferences.getBoolean("buttonToggle7", false)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zr, + R.drawable.button_zr_pressed, ButtonType.BUTTON_ZR, orientation)); + } + if (mPreferences.getBoolean("buttonToggle8", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_start, + R.drawable.button_start_pressed, ButtonType.BUTTON_START, orientation)); + } + if (mPreferences.getBoolean("buttonToggle9", true)) { + overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_select, + R.drawable.button_select_pressed, ButtonType.BUTTON_SELECT, orientation)); + } + if (mPreferences.getBoolean("buttonToggle10", true)) { + overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad, + R.drawable.dpad_pressed_one_direction, + R.drawable.dpad_pressed_two_directions, + ButtonType.DPAD_UP, ButtonType.DPAD_DOWN, + ButtonType.DPAD_LEFT, ButtonType.DPAD_RIGHT, orientation)); + } + if (mPreferences.getBoolean("buttonToggle11", true)) { + overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_main_range, + R.drawable.stick_main, R.drawable.stick_main_pressed, + ButtonType.STICK_LEFT, orientation)); + } + if (mPreferences.getBoolean("buttonToggle12", false)) { + overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_c_range, + R.drawable.stick_c, R.drawable.stick_c_pressed, ButtonType.STICK_C, orientation)); + } + } + + public void refreshControls() { + // Remove all the overlay buttons from the HashSet. + overlayButtons.clear(); + overlayDpads.clear(); + overlayJoysticks.clear(); + + String orientation = + getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? + "-Portrait" : ""; + + // Add all the enabled overlay items back to the HashSet. + if (EmulationMenuSettings.getShowOverlay()) { + addOverlayControls(orientation); + } + + invalidate(); + } + + private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) { + final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor sPrefsEditor = sPrefs.edit(); + sPrefsEditor.putFloat(sharedPrefsId + orientation + "-X", x); + sPrefsEditor.putFloat(sharedPrefsId + orientation + "-Y", y); + sPrefsEditor.apply(); + } + + public void setIsInEditMode(boolean isInEditMode) { + mIsInEditMode = isInEditMode; + } + + private void defaultOverlay() { + if (!mPreferences.getBoolean("OverlayInit", false)) { + // It's possible that a user has created their overlay before this was added + // Only change the overlay if the 'A' button is not in the upper corner. + if (mPreferences.getFloat(ButtonType.BUTTON_A + "-X", 0f) == 0f) { + defaultOverlayLandscape(); + } + if (mPreferences.getFloat(ButtonType.BUTTON_A + "-Portrait" + "-X", 0f) == 0f) { + defaultOverlayPortrait(); + } + } + + SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); + sPrefsEditor.putBoolean("OverlayInit", true); + sPrefsEditor.apply(); + } + + public void resetButtonPlacement() { + boolean isLandscape = + getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + + if (isLandscape) { + defaultOverlayLandscape(); + } else { + defaultOverlayPortrait(); + } + + refreshControls(); + } + + private void defaultOverlayLandscape() { + SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); + // Get screen size + Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); + DisplayMetrics outMetrics = new DisplayMetrics(); + display.getMetrics(outMetrics); + float maxX = outMetrics.heightPixels; + float maxY = outMetrics.widthPixels; + // Height and width changes depending on orientation. Use the larger value for height. + if (maxY > maxX) { + float tmp = maxX; + maxX = maxY; + maxY = tmp; + } + Resources res = getResources(); + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.STICK_C + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.STICK_C + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_Y) / 1000) * maxY)); + + // We want to commit right away, otherwise the overlay could load before this is saved. + sPrefsEditor.commit(); + } + + private void defaultOverlayPortrait() { + SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); + // Get screen size + Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); + DisplayMetrics outMetrics = new DisplayMetrics(); + display.getMetrics(outMetrics); + float maxX = outMetrics.heightPixels; + float maxY = outMetrics.widthPixels; + // Height and width changes depending on orientation. Use the larger value for height. + if (maxY < maxX) { + float tmp = maxX; + maxX = maxY; + maxY = tmp; + } + Resources res = getResources(); + String portrait = "-Portrait"; + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_Y) / 1000) * maxY)); + sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_X) / 1000) * maxX)); + sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_Y) / 1000) * maxY)); + + // We want to commit right away, otherwise the overlay could load before this is saved. + sPrefsEditor.commit(); + } + + public boolean isInEditMode() { + return mIsInEditMode; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java new file mode 100644 index 000000000..81352296c --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java @@ -0,0 +1,122 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.citra.citra_emu.overlay; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.view.MotionEvent; + +/** + * Custom {@link BitmapDrawable} that is capable + * of storing it's own ID. + */ +public final class InputOverlayDrawableButton { + // The ID identifying what type of button this Drawable represents. + private int mButtonType; + private int mTrackId; + private int mPreviousTouchX, mPreviousTouchY; + private int mControlPositionX, mControlPositionY; + private int mWidth; + private int mHeight; + private BitmapDrawable mDefaultStateBitmap; + private BitmapDrawable mPressedStateBitmap; + private boolean mPressedState = false; + + /** + * Constructor + * + * @param res {@link Resources} instance. + * @param defaultStateBitmap {@link Bitmap} to use with the default state Drawable. + * @param pressedStateBitmap {@link Bitmap} to use with the pressed state Drawable. + * @param buttonType Identifier for this type of button. + */ + public InputOverlayDrawableButton(Resources res, Bitmap defaultStateBitmap, + Bitmap pressedStateBitmap, int buttonType) { + mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); + mPressedStateBitmap = new BitmapDrawable(res, pressedStateBitmap); + mButtonType = buttonType; + + mWidth = mDefaultStateBitmap.getIntrinsicWidth(); + mHeight = mDefaultStateBitmap.getIntrinsicHeight(); + } + + /** + * Gets this InputOverlayDrawableButton's button ID. + * + * @return this InputOverlayDrawableButton's button ID. + */ + public int getId() { + return mButtonType; + } + + public int getTrackId() { + return mTrackId; + } + + public void setTrackId(int trackId) { + mTrackId = trackId; + } + + public boolean onConfigureTouch(MotionEvent event) { + int pointerIndex = event.getActionIndex(); + int fingerPositionX = (int) event.getX(pointerIndex); + int fingerPositionY = (int) event.getY(pointerIndex); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + case MotionEvent.ACTION_MOVE: + mControlPositionX += fingerPositionX - mPreviousTouchX; + mControlPositionY += fingerPositionY - mPreviousTouchY; + setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, + getHeight() + mControlPositionY); + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + + } + return true; + } + + public void setPosition(int x, int y) { + mControlPositionX = x; + mControlPositionY = y; + } + + public void draw(Canvas canvas) { + getCurrentStateBitmapDrawable().draw(canvas); + } + + private BitmapDrawable getCurrentStateBitmapDrawable() { + return mPressedState ? mPressedStateBitmap : mDefaultStateBitmap; + } + + public void setBounds(int left, int top, int right, int bottom) { + mDefaultStateBitmap.setBounds(left, top, right, bottom); + mPressedStateBitmap.setBounds(left, top, right, bottom); + } + + public Rect getBounds() { + return mDefaultStateBitmap.getBounds(); + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public void setPressedState(boolean isPressed) { + mPressedState = isPressed; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java new file mode 100644 index 000000000..87f3b7cd9 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java @@ -0,0 +1,193 @@ +/** + * Copyright 2016 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.citra.citra_emu.overlay; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.view.MotionEvent; + +/** + * Custom {@link BitmapDrawable} that is capable + * of storing it's own ID. + */ +public final class InputOverlayDrawableDpad { + public static final int STATE_DEFAULT = 0; + public static final int STATE_PRESSED_UP = 1; + public static final int STATE_PRESSED_DOWN = 2; + public static final int STATE_PRESSED_LEFT = 3; + public static final int STATE_PRESSED_RIGHT = 4; + public static final int STATE_PRESSED_UP_LEFT = 5; + public static final int STATE_PRESSED_UP_RIGHT = 6; + public static final int STATE_PRESSED_DOWN_LEFT = 7; + public static final int STATE_PRESSED_DOWN_RIGHT = 8; + public static final float VIRT_AXIS_DEADZONE = 0.5f; + // The ID identifying what type of button this Drawable represents. + private int[] mButtonType = new int[4]; + private int mTrackId; + private int mPreviousTouchX, mPreviousTouchY; + private int mControlPositionX, mControlPositionY; + private int mWidth; + private int mHeight; + private BitmapDrawable mDefaultStateBitmap; + private BitmapDrawable mPressedOneDirectionStateBitmap; + private BitmapDrawable mPressedTwoDirectionsStateBitmap; + private int mPressState = STATE_DEFAULT; + + /** + * Constructor + * + * @param res {@link Resources} instance. + * @param defaultStateBitmap {@link Bitmap} of the default state. + * @param pressedOneDirectionStateBitmap {@link Bitmap} of the pressed state in one direction. + * @param pressedTwoDirectionsStateBitmap {@link Bitmap} of the pressed state in two direction. + * @param buttonUp Identifier for the up button. + * @param buttonDown Identifier for the down button. + * @param buttonLeft Identifier for the left button. + * @param buttonRight Identifier for the right button. + */ + public InputOverlayDrawableDpad(Resources res, + Bitmap defaultStateBitmap, + Bitmap pressedOneDirectionStateBitmap, + Bitmap pressedTwoDirectionsStateBitmap, + int buttonUp, int buttonDown, + int buttonLeft, int buttonRight) { + mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap); + mPressedOneDirectionStateBitmap = new BitmapDrawable(res, pressedOneDirectionStateBitmap); + mPressedTwoDirectionsStateBitmap = new BitmapDrawable(res, pressedTwoDirectionsStateBitmap); + + mWidth = mDefaultStateBitmap.getIntrinsicWidth(); + mHeight = mDefaultStateBitmap.getIntrinsicHeight(); + + mButtonType[0] = buttonUp; + mButtonType[1] = buttonDown; + mButtonType[2] = buttonLeft; + mButtonType[3] = buttonRight; + + mTrackId = -1; + } + + public void draw(Canvas canvas) { + int px = mControlPositionX + (getWidth() / 2); + int py = mControlPositionY + (getHeight() / 2); + switch (mPressState) { + case STATE_DEFAULT: + mDefaultStateBitmap.draw(canvas); + break; + case STATE_PRESSED_UP: + mPressedOneDirectionStateBitmap.draw(canvas); + break; + case STATE_PRESSED_RIGHT: + canvas.save(); + canvas.rotate(90, px, py); + mPressedOneDirectionStateBitmap.draw(canvas); + canvas.restore(); + break; + case STATE_PRESSED_DOWN: + canvas.save(); + canvas.rotate(180, px, py); + mPressedOneDirectionStateBitmap.draw(canvas); + canvas.restore(); + break; + case STATE_PRESSED_LEFT: + canvas.save(); + canvas.rotate(270, px, py); + mPressedOneDirectionStateBitmap.draw(canvas); + canvas.restore(); + break; + case STATE_PRESSED_UP_LEFT: + mPressedTwoDirectionsStateBitmap.draw(canvas); + break; + case STATE_PRESSED_UP_RIGHT: + canvas.save(); + canvas.rotate(90, px, py); + mPressedTwoDirectionsStateBitmap.draw(canvas); + canvas.restore(); + break; + case STATE_PRESSED_DOWN_RIGHT: + canvas.save(); + canvas.rotate(180, px, py); + mPressedTwoDirectionsStateBitmap.draw(canvas); + canvas.restore(); + break; + case STATE_PRESSED_DOWN_LEFT: + canvas.save(); + canvas.rotate(270, px, py); + mPressedTwoDirectionsStateBitmap.draw(canvas); + canvas.restore(); + break; + } + } + + /** + * Gets one of the InputOverlayDrawableDpad's button IDs. + * + * @return the requested InputOverlayDrawableDpad's button ID. + */ + public int getId(int direction) { + return mButtonType[direction]; + } + + public int getTrackId() { + return mTrackId; + } + + public void setTrackId(int trackId) { + mTrackId = trackId; + } + + public boolean onConfigureTouch(MotionEvent event) { + int pointerIndex = event.getActionIndex(); + int fingerPositionX = (int) event.getX(pointerIndex); + int fingerPositionY = (int) event.getY(pointerIndex); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + case MotionEvent.ACTION_MOVE: + mControlPositionX += fingerPositionX - mPreviousTouchX; + mControlPositionY += fingerPositionY - mPreviousTouchY; + setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX, + getHeight() + mControlPositionY); + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + + } + return true; + } + + public void setPosition(int x, int y) { + mControlPositionX = x; + mControlPositionY = y; + } + + public void setBounds(int left, int top, int right, int bottom) { + mDefaultStateBitmap.setBounds(left, top, right, bottom); + mPressedOneDirectionStateBitmap.setBounds(left, top, right, bottom); + mPressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom); + } + + public Rect getBounds() { + return mDefaultStateBitmap.getBounds(); + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public void setState(int pressState) { + mPressState = pressState; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java new file mode 100644 index 000000000..956a8b1e9 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java @@ -0,0 +1,264 @@ +/** + * Copyright 2013 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.citra.citra_emu.overlay; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.view.MotionEvent; + +import org.citra.citra_emu.NativeLibrary.ButtonType; +import org.citra.citra_emu.utils.EmulationMenuSettings; + +/** + * Custom {@link BitmapDrawable} that is capable + * of storing it's own ID. + */ +public final class InputOverlayDrawableJoystick { + private final int[] axisIDs = {0, 0, 0, 0}; + private final float[] axises = {0f, 0f}; + private int trackId = -1; + private int mJoystickType; + private int mControlPositionX, mControlPositionY; + private int mPreviousTouchX, mPreviousTouchY; + private int mWidth; + private int mHeight; + private Rect mVirtBounds; + private Rect mOrigBounds; + private BitmapDrawable mOuterBitmap; + private BitmapDrawable mDefaultStateInnerBitmap; + private BitmapDrawable mPressedStateInnerBitmap; + private BitmapDrawable mBoundsBoxBitmap; + private boolean mPressedState = false; + + /** + * Constructor + * + * @param res {@link Resources} instance. + * @param bitmapOuter {@link Bitmap} which represents the outer non-movable part of the joystick. + * @param bitmapInnerDefault {@link Bitmap} which represents the default inner movable part of the joystick. + * @param bitmapInnerPressed {@link Bitmap} which represents the pressed inner movable part of the joystick. + * @param rectOuter {@link Rect} which represents the outer joystick bounds. + * @param rectInner {@link Rect} which represents the inner joystick bounds. + * @param joystick Identifier for which joystick this is. + */ + public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter, + Bitmap bitmapInnerDefault, Bitmap bitmapInnerPressed, + Rect rectOuter, Rect rectInner, int joystick) { + axisIDs[0] = joystick + 1; // Up + axisIDs[1] = joystick + 2; // Down + axisIDs[2] = joystick + 3; // Left + axisIDs[3] = joystick + 4; // Right + mJoystickType = joystick; + + mOuterBitmap = new BitmapDrawable(res, bitmapOuter); + mDefaultStateInnerBitmap = new BitmapDrawable(res, bitmapInnerDefault); + mPressedStateInnerBitmap = new BitmapDrawable(res, bitmapInnerPressed); + mBoundsBoxBitmap = new BitmapDrawable(res, bitmapOuter); + mWidth = bitmapOuter.getWidth(); + mHeight = bitmapOuter.getHeight(); + + setBounds(rectOuter); + mDefaultStateInnerBitmap.setBounds(rectInner); + mPressedStateInnerBitmap.setBounds(rectInner); + mVirtBounds = getBounds(); + mOrigBounds = mOuterBitmap.copyBounds(); + mBoundsBoxBitmap.setAlpha(0); + mBoundsBoxBitmap.setBounds(getVirtBounds()); + SetInnerBounds(); + } + + /** + * Gets this InputOverlayDrawableJoystick's button ID. + * + * @return this InputOverlayDrawableJoystick's button ID. + */ + public int getId() { + return mJoystickType; + } + + public void draw(Canvas canvas) { + mOuterBitmap.draw(canvas); + getCurrentStateBitmapDrawable().draw(canvas); + mBoundsBoxBitmap.draw(canvas); + } + + public void TrackEvent(MotionEvent event) { + int pointerIndex = event.getActionIndex(); + + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + if (getBounds().contains((int) event.getX(pointerIndex), (int) event.getY(pointerIndex))) { + mPressedState = true; + mOuterBitmap.setAlpha(0); + mBoundsBoxBitmap.setAlpha(255); + if (EmulationMenuSettings.getJoystickRelCenter()) { + getVirtBounds().offset((int) event.getX(pointerIndex) - getVirtBounds().centerX(), + (int) event.getY(pointerIndex) - getVirtBounds().centerY()); + } + mBoundsBoxBitmap.setBounds(getVirtBounds()); + trackId = event.getPointerId(pointerIndex); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + if (trackId == event.getPointerId(pointerIndex)) { + mPressedState = false; + axises[0] = axises[1] = 0.0f; + mOuterBitmap.setAlpha(255); + mBoundsBoxBitmap.setAlpha(0); + setVirtBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, + mOrigBounds.bottom)); + setBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right, + mOrigBounds.bottom)); + SetInnerBounds(); + trackId = -1; + } + break; + } + + if (trackId == -1) + return; + + for (int i = 0; i < event.getPointerCount(); i++) { + if (trackId == event.getPointerId(i)) { + float touchX = event.getX(i); + float touchY = event.getY(i); + float maxY = getVirtBounds().bottom; + float maxX = getVirtBounds().right; + touchX -= getVirtBounds().centerX(); + maxX -= getVirtBounds().centerX(); + touchY -= getVirtBounds().centerY(); + maxY -= getVirtBounds().centerY(); + final float AxisX = touchX / maxX; + final float AxisY = touchY / maxY; + + // Clamp the circle pad input to a circle + final float angle = (float) Math.atan2(AxisY, AxisX); + float radius = (float) Math.sqrt(AxisX * AxisX + AxisY * AxisY); + if(radius > 1.0f) + { + radius = 1.0f; + } + axises[0] = ((float)Math.cos(angle) * radius); + axises[1] = ((float)Math.sin(angle) * radius); + SetInnerBounds(); + } + } + } + + public boolean onConfigureTouch(MotionEvent event) { + int pointerIndex = event.getActionIndex(); + int fingerPositionX = (int) event.getX(pointerIndex); + int fingerPositionY = (int) event.getY(pointerIndex); + + int scale = 1; + if (mJoystickType == ButtonType.STICK_C) { + // C-stick is scaled down to be half the size of the circle pad + scale = 2; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + case MotionEvent.ACTION_MOVE: + int deltaX = fingerPositionX - mPreviousTouchX; + int deltaY = fingerPositionY - mPreviousTouchY; + mControlPositionX += deltaX; + mControlPositionY += deltaY; + setBounds(new Rect(mControlPositionX, mControlPositionY, + mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, + mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)); + setVirtBounds(new Rect(mControlPositionX, mControlPositionY, + mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, + mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)); + SetInnerBounds(); + setOrigBounds(new Rect(new Rect(mControlPositionX, mControlPositionY, + mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX, + mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY))); + mPreviousTouchX = fingerPositionX; + mPreviousTouchY = fingerPositionY; + break; + } + return true; + } + + + public float[] getAxisValues() { + return axises; + } + + public int[] getAxisIDs() { + return axisIDs; + } + + private void SetInnerBounds() { + int X = getVirtBounds().centerX() + (int) ((axises[0]) * (getVirtBounds().width() / 2)); + int Y = getVirtBounds().centerY() + (int) ((axises[1]) * (getVirtBounds().height() / 2)); + + if (mJoystickType == ButtonType.STICK_LEFT) { + X += 1; + Y += 1; + } + + if (X > getVirtBounds().centerX() + (getVirtBounds().width() / 2)) + X = getVirtBounds().centerX() + (getVirtBounds().width() / 2); + if (X < getVirtBounds().centerX() - (getVirtBounds().width() / 2)) + X = getVirtBounds().centerX() - (getVirtBounds().width() / 2); + if (Y > getVirtBounds().centerY() + (getVirtBounds().height() / 2)) + Y = getVirtBounds().centerY() + (getVirtBounds().height() / 2); + if (Y < getVirtBounds().centerY() - (getVirtBounds().height() / 2)) + Y = getVirtBounds().centerY() - (getVirtBounds().height() / 2); + + int width = mPressedStateInnerBitmap.getBounds().width() / 2; + int height = mPressedStateInnerBitmap.getBounds().height() / 2; + mDefaultStateInnerBitmap.setBounds(X - width, Y - height, X + width, Y + height); + mPressedStateInnerBitmap.setBounds(mDefaultStateInnerBitmap.getBounds()); + } + + public void setPosition(int x, int y) { + mControlPositionX = x; + mControlPositionY = y; + } + + private BitmapDrawable getCurrentStateBitmapDrawable() { + return mPressedState ? mPressedStateInnerBitmap : mDefaultStateInnerBitmap; + } + + public Rect getBounds() { + return mOuterBitmap.getBounds(); + } + + public void setBounds(Rect bounds) { + mOuterBitmap.setBounds(bounds); + } + + private void setOrigBounds(Rect bounds) { + mOrigBounds = bounds; + } + + private Rect getVirtBounds() { + return mVirtBounds; + } + + private void setVirtBounds(Rect bounds) { + mVirtBounds = bounds; + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java new file mode 100644 index 000000000..96ccc08bb --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java @@ -0,0 +1,130 @@ +package org.citra.citra_emu.ui; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Implementation from: + * https://gist.github.com/lapastillaroja/858caf1a82791b6c1a36 + */ +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + + private Drawable mDivider; + private boolean mShowFirstDivider = false; + private boolean mShowLastDivider = false; + + public DividerItemDecoration(Context context, AttributeSet attrs) { + final TypedArray a = context + .obtainStyledAttributes(attrs, new int[]{android.R.attr.listDivider}); + mDivider = a.getDrawable(0); + a.recycle(); + } + + public DividerItemDecoration(Context context, AttributeSet attrs, boolean showFirstDivider, + boolean showLastDivider) { + this(context, attrs); + mShowFirstDivider = showFirstDivider; + mShowLastDivider = showLastDivider; + } + + public DividerItemDecoration(Drawable divider) { + mDivider = divider; + } + + public DividerItemDecoration(Drawable divider, boolean showFirstDivider, + boolean showLastDivider) { + this(divider); + mShowFirstDivider = showFirstDivider; + mShowLastDivider = showLastDivider; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, + @NonNull RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + if (mDivider == null) { + return; + } + if (parent.getChildAdapterPosition(view) < 1) { + return; + } + + if (getOrientation(parent) == LinearLayoutManager.VERTICAL) { + outRect.top = mDivider.getIntrinsicHeight(); + } else { + outRect.left = mDivider.getIntrinsicWidth(); + } + } + + @Override + public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + if (mDivider == null) { + super.onDrawOver(c, parent, state); + return; + } + + // Initialization needed to avoid compiler warning + int left = 0, right = 0, top = 0, bottom = 0, size; + int orientation = getOrientation(parent); + int childCount = parent.getChildCount(); + + if (orientation == LinearLayoutManager.VERTICAL) { + size = mDivider.getIntrinsicHeight(); + left = parent.getPaddingLeft(); + right = parent.getWidth() - parent.getPaddingRight(); + } else { //horizontal + size = mDivider.getIntrinsicWidth(); + top = parent.getPaddingTop(); + bottom = parent.getHeight() - parent.getPaddingBottom(); + } + + for (int i = mShowFirstDivider ? 0 : 1; i < childCount; i++) { + View child = parent.getChildAt(i); + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); + + if (orientation == LinearLayoutManager.VERTICAL) { + top = child.getTop() - params.topMargin; + bottom = top + size; + } else { //horizontal + left = child.getLeft() - params.leftMargin; + right = left + size; + } + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + + // show last divider + if (mShowLastDivider && childCount > 0) { + View child = parent.getChildAt(childCount - 1); + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); + if (orientation == LinearLayoutManager.VERTICAL) { + top = child.getBottom() + params.bottomMargin; + bottom = top + size; + } else { // horizontal + left = child.getRight() + params.rightMargin; + right = left + size; + } + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + private int getOrientation(RecyclerView parent) { + if (parent.getLayoutManager() instanceof LinearLayoutManager) { + LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager(); + return layoutManager.getOrientation(); + } else { + throw new IllegalStateException( + "DividerItemDecoration can only be used with a LinearLayoutManager."); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java new file mode 100644 index 000000000..402c8a4e0 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.java @@ -0,0 +1,269 @@ +package org.citra.citra_emu.ui.main; + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +import org.citra.citra_emu.NativeLibrary; +import org.citra.citra_emu.R; +import org.citra.citra_emu.activities.EmulationActivity; +import org.citra.citra_emu.features.settings.ui.SettingsActivity; +import org.citra.citra_emu.model.GameProvider; +import org.citra.citra_emu.ui.platform.PlatformGamesFragment; +import org.citra.citra_emu.utils.AddDirectoryHelper; +import org.citra.citra_emu.utils.BillingManager; +import org.citra.citra_emu.utils.DirectoryInitialization; +import org.citra.citra_emu.utils.FileBrowserHelper; +import org.citra.citra_emu.utils.PermissionsHandler; +import org.citra.citra_emu.utils.PicassoUtils; +import org.citra.citra_emu.utils.StartupHandler; +import org.citra.citra_emu.utils.ThemeUtil; + +import java.util.Arrays; +import java.util.Collections; + +/** + * The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which + * individually display a grid of available games for each Fragment, in a tabbed layout. + */ +public final class MainActivity extends AppCompatActivity implements MainView { + private Toolbar mToolbar; + private int mFrameLayoutId; + private PlatformGamesFragment mPlatformGamesFragment; + + private MainPresenter mPresenter = new MainPresenter(this); + + // Singleton to manage user billing state + private static BillingManager mBillingManager; + + private static MenuItem mPremiumButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + ThemeUtil.applyTheme(); + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + findViews(); + + setSupportActionBar(mToolbar); + + mFrameLayoutId = R.id.games_platform_frame; + mPresenter.onCreate(); + + if (savedInstanceState == null) { + StartupHandler.HandleInit(this); + if (PermissionsHandler.hasWriteAccess(this)) { + mPlatformGamesFragment = new PlatformGamesFragment(); + getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment) + .commit(); + } + } else { + mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment"); + } + PicassoUtils.init(); + + // Setup billing manager, so we can globally query for Premium status + mBillingManager = new BillingManager(this); + + // Dismiss previous notifications (should not happen unless a crash occurred) + EmulationActivity.tryDismissRunningNotification(this); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (PermissionsHandler.hasWriteAccess(this)) { + if (getSupportFragmentManager() == null) { + return; + } + if (outState == null) { + return; + } + getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment); + } + } + + @Override + protected void onResume() { + super.onResume(); + mPresenter.addDirIfNeeded(new AddDirectoryHelper(this)); + } + + // TODO: Replace with a ButterKnife injection. + private void findViews() { + mToolbar = findViewById(R.id.toolbar_main); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_game_grid, menu); + mPremiumButton = menu.findItem(R.id.button_premium); + + if (mBillingManager.isPremiumCached()) { + // User had premium in a previous session, hide upsell option + setPremiumButtonVisible(false); + } + + return true; + } + + static public void setPremiumButtonVisible(boolean isVisible) { + if (mPremiumButton != null) { + mPremiumButton.setVisible(isVisible); + } + } + + /** + * MainView + */ + + @Override + public void setVersionString(String version) { + mToolbar.setSubtitle(version); + } + + @Override + public void refresh() { + getContentResolver().insert(GameProvider.URI_REFRESH, null); + refreshFragment(); + } + + @Override + public void launchSettingsActivity(String menuTag) { + if (PermissionsHandler.hasWriteAccess(this)) { + SettingsActivity.launch(this, menuTag, ""); + } else { + PermissionsHandler.checkWritePermission(this); + } + } + + @Override + public void launchFileListActivity(int request) { + if (PermissionsHandler.hasWriteAccess(this)) { + switch (request) { + case MainPresenter.REQUEST_ADD_DIRECTORY: + FileBrowserHelper.openDirectoryPicker(this, + MainPresenter.REQUEST_ADD_DIRECTORY, + R.string.select_game_folder, + Arrays.asList("elf", "axf", "cci", "3ds", + "cxi", "app", "3dsx", "cia", + "rar", "zip", "7z", "torrent", + "tar", "gz")); + break; + case MainPresenter.REQUEST_INSTALL_CIA: + FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_INSTALL_CIA, + R.string.install_cia_title, + Collections.singletonList("cia"), true); + break; + } + } else { + PermissionsHandler.checkWritePermission(this); + } + } + + /** + * @param requestCode An int describing whether the Activity that is returning did so successfully. + * @param resultCode An int describing what Activity is giving us this callback. + * @param result The information the returning Activity is providing us. + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent result) { + super.onActivityResult(requestCode, resultCode, result); + switch (requestCode) { + case MainPresenter.REQUEST_ADD_DIRECTORY: + // If the user picked a file, as opposed to just backing out. + if (resultCode == MainActivity.RESULT_OK) { + // When a new directory is picked, we currently will reset the existing games + // database. This effectively means that only one game directory is supported. + // TODO(bunnei): Consider fixing this in the future, or removing code for this. + getContentResolver().insert(GameProvider.URI_RESET, null); + // Add the new directory + mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedDirectory(result)); + } + break; + case MainPresenter.REQUEST_INSTALL_CIA: + // If the user picked a file, as opposed to just backing out. + if (resultCode == MainActivity.RESULT_OK) { + NativeLibrary.InstallCIAS(FileBrowserHelper.getSelectedFiles(result)); + mPresenter.refeshGameList(); + } + break; + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION: + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + DirectoryInitialization.start(this); + + mPlatformGamesFragment = new PlatformGamesFragment(); + getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment) + .commit(); + + // Immediately prompt user to select a game directory on first boot + if (mPresenter != null) { + mPresenter.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY); + } + } else { + Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT) + .show(); + } + break; + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + break; + } + } + + /** + * Called by the framework whenever any actionbar/toolbar icon is clicked. + * + * @param item The icon that was clicked on. + * @return True if the event was handled, false to bubble it up to the OS. + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return mPresenter.handleOptionSelection(item.getItemId()); + } + + private void refreshFragment() { + if (mPlatformGamesFragment != null) { + mPlatformGamesFragment.refresh(); + } + } + + @Override + protected void onDestroy() { + EmulationActivity.tryDismissRunningNotification(this); + super.onDestroy(); + } + + /** + * @return true if Premium subscription is currently active + */ + public static boolean isPremiumActive() { + return mBillingManager.isPremiumActive(); + } + + /** + * Invokes the billing flow for Premium + * + * @param callback Optional callback, called once, on completion of billing + */ + public static void invokePremiumBilling(Runnable callback) { + mBillingManager.invokePremiumBilling(callback); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainPresenter.java new file mode 100644 index 000000000..4e9994c2a --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainPresenter.java @@ -0,0 +1,82 @@ +package org.citra.citra_emu.ui.main; + +import android.os.SystemClock; + +import org.citra.citra_emu.BuildConfig; +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.model.Settings; +import org.citra.citra_emu.features.settings.utils.SettingsFile; +import org.citra.citra_emu.model.GameDatabase; +import org.citra.citra_emu.utils.AddDirectoryHelper; + +public final class MainPresenter { + public static final int REQUEST_ADD_DIRECTORY = 1; + public static final int REQUEST_INSTALL_CIA = 2; + + private final MainView mView; + private String mDirToAdd; + private long mLastClickTime = 0; + + public MainPresenter(MainView view) { + mView = view; + } + + public void onCreate() { + String versionName = BuildConfig.VERSION_NAME; + mView.setVersionString(versionName); + refeshGameList(); + } + + public void launchFileListActivity(int request) { + if (mView != null) { + mView.launchFileListActivity(request); + } + } + + public boolean handleOptionSelection(int itemId) { + // Double-click prevention, using threshold of 500 ms + if (SystemClock.elapsedRealtime() - mLastClickTime < 500) { + return false; + } + mLastClickTime = SystemClock.elapsedRealtime(); + + switch (itemId) { + case R.id.menu_settings_core: + mView.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG); + return true; + + case R.id.button_add_directory: + launchFileListActivity(REQUEST_ADD_DIRECTORY); + return true; + + case R.id.button_install_cia: + launchFileListActivity(REQUEST_INSTALL_CIA); + return true; + + case R.id.button_premium: + mView.launchSettingsActivity(Settings.SECTION_PREMIUM); + return true; + } + + return false; + } + + public void addDirIfNeeded(AddDirectoryHelper helper) { + if (mDirToAdd != null) { + helper.addDirectory(mDirToAdd, mView::refresh); + + mDirToAdd = null; + } + } + + public void onDirectorySelected(String dir) { + mDirToAdd = dir; + } + + public void refeshGameList() { + GameDatabase databaseHelper = CitraApplication.databaseHelper; + databaseHelper.scanLibrary(databaseHelper.getWritableDatabase()); + mView.refresh(); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainView.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainView.java new file mode 100644 index 000000000..de7c04875 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainView.java @@ -0,0 +1,25 @@ +package org.citra.citra_emu.ui.main; + +/** + * Abstraction for the screen that shows on application launch. + * Implementations will differ primarily to target touch-screen + * or non-touch screen devices. + */ +public interface MainView { + /** + * Pass the view the native library's version string. Displaying + * it is optional. + * + * @param version A string pulled from native code. + */ + void setVersionString(String version); + + /** + * Tell the view to refresh its contents. + */ + void refresh(); + + void launchSettingsActivity(String menuTag); + + void launchFileListActivity(int request); +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesFragment.java new file mode 100644 index 000000000..9fc30796f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesFragment.java @@ -0,0 +1,86 @@ +package org.citra.citra_emu.ui.platform; + +import android.database.Cursor; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.R; +import org.citra.citra_emu.adapters.GameAdapter; +import org.citra.citra_emu.model.GameDatabase; + +public final class PlatformGamesFragment extends Fragment implements PlatformGamesView { + private PlatformGamesPresenter mPresenter = new PlatformGamesPresenter(this); + + private GameAdapter mAdapter; + private RecyclerView mRecyclerView; + private TextView mTextView; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_grid, container, false); + + findViews(rootView); + + mPresenter.onCreateView(); + + return rootView; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + int columns = getResources().getInteger(R.integer.game_grid_columns); + RecyclerView.LayoutManager layoutManager = new GridLayoutManager(getActivity(), columns); + mAdapter = new GameAdapter(); + + mRecyclerView.setLayoutManager(layoutManager); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.addItemDecoration(new GameAdapter.SpacesItemDecoration(ContextCompat.getDrawable(getActivity(), R.drawable.gamelist_divider), 1)); + + // Add swipe down to refresh gesture + final SwipeRefreshLayout pullToRefresh = view.findViewById(R.id.refresh_grid_games); + pullToRefresh.setOnRefreshListener(() -> { + GameDatabase databaseHelper = CitraApplication.databaseHelper; + databaseHelper.scanLibrary(databaseHelper.getWritableDatabase()); + refresh(); + pullToRefresh.setRefreshing(false); + }); + } + + @Override + public void refresh() { + mPresenter.refresh(); + updateTextView(); + } + + @Override + public void showGames(Cursor games) { + if (mAdapter != null) { + mAdapter.swapCursor(games); + } + updateTextView(); + } + + private void updateTextView() { + mTextView.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + + private void findViews(View root) { + mRecyclerView = root.findViewById(R.id.grid_games); + mTextView = root.findViewById(R.id.gamelist_empty_text); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesPresenter.java new file mode 100644 index 000000000..9d8040e1b --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesPresenter.java @@ -0,0 +1,42 @@ +package org.citra.citra_emu.ui.platform; + + +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.model.GameDatabase; +import org.citra.citra_emu.utils.Log; + +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; + +public final class PlatformGamesPresenter { + private final PlatformGamesView mView; + + public PlatformGamesPresenter(PlatformGamesView view) { + mView = view; + } + + public void onCreateView() { + loadGames(); + } + + public void refresh() { + Log.debug("[PlatformGamesPresenter] : Refreshing..."); + loadGames(); + } + + private void loadGames() { + Log.debug("[PlatformGamesPresenter] : Loading games..."); + + GameDatabase databaseHelper = CitraApplication.databaseHelper; + + databaseHelper.getGames() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(games -> + { + Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor..."); + + mView.showGames(games); + }); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesView.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesView.java new file mode 100644 index 000000000..4332121eb --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/platform/PlatformGamesView.java @@ -0,0 +1,21 @@ +package org.citra.citra_emu.ui.platform; + +import android.database.Cursor; + +/** + * Abstraction for a screen representing a single platform's games. + */ +public interface PlatformGamesView { + /** + * Tell the view to refresh its contents. + */ + void refresh(); + + /** + * To be called when an asynchronous database read completes. Passes the + * result, in this case a {@link Cursor}, to the view. + * + * @param games A Cursor containing the games read from the database. + */ + void showGames(Cursor games); +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java new file mode 100644 index 000000000..886846ec5 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java @@ -0,0 +1,5 @@ +package org.citra.citra_emu.utils; + +public interface Action1<T> { + void call(T t); +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/AddDirectoryHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/AddDirectoryHelper.java new file mode 100644 index 000000000..7578c353f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/AddDirectoryHelper.java @@ -0,0 +1,38 @@ +package org.citra.citra_emu.utils; + +import android.content.AsyncQueryHandler; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; + +import org.citra.citra_emu.model.GameDatabase; +import org.citra.citra_emu.model.GameProvider; + +public class AddDirectoryHelper { + private Context mContext; + + public AddDirectoryHelper(Context context) { + this.mContext = context; + } + + public void addDirectory(String dir, AddDirectoryListener addDirectoryListener) { + AsyncQueryHandler handler = new AsyncQueryHandler(mContext.getContentResolver()) { + @Override + protected void onInsertComplete(int token, Object cookie, Uri uri) { + addDirectoryListener.onDirectoryAdded(); + } + }; + + ContentValues file = new ContentValues(); + file.put(GameDatabase.KEY_FOLDER_PATH, dir); + + handler.startInsert(0, // We don't need to identify this call to the handler + null, // We don't need to pass additional data to the handler + GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder + file); + } + + public interface AddDirectoryListener { + void onDirectoryAdded(); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java new file mode 100644 index 000000000..dfbab1780 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java @@ -0,0 +1,22 @@ +package org.citra.citra_emu.utils; + +import java.util.HashMap; +import java.util.Map; + +public class BiMap<K, V> { + private Map<K, V> forward = new HashMap<K, V>(); + private Map<V, K> backward = new HashMap<V, K>(); + + public synchronized void add(K key, V value) { + forward.put(key, value); + backward.put(value, key); + } + + public synchronized V getForward(K key) { + return forward.get(key); + } + + public synchronized K getBackward(V key) { + return backward.get(key); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/BillingManager.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/BillingManager.java new file mode 100644 index 000000000..5dc54c235 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/BillingManager.java @@ -0,0 +1,215 @@ +package org.citra.citra_emu.utils; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.widget.Toast; + +import com.android.billingclient.api.AcknowledgePurchaseParams; +import com.android.billingclient.api.AcknowledgePurchaseResponseListener; +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingClientStateListener; +import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.Purchase.PurchasesResult; +import com.android.billingclient.api.PurchasesUpdatedListener; +import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.SkuDetailsParams; + +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.R; +import org.citra.citra_emu.features.settings.utils.SettingsFile; +import org.citra.citra_emu.ui.main.MainActivity; + +import java.util.ArrayList; +import java.util.List; + +public class BillingManager implements PurchasesUpdatedListener { + private final String BILLING_SKU_PREMIUM = "citra.citra_emu.product_id.premium"; + + private final Activity mActivity; + private BillingClient mBillingClient; + private SkuDetails mSkuPremium; + private boolean mIsPremiumActive = false; + private boolean mIsServiceConnected = false; + private Runnable mUpdateBillingCallback; + + private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); + + public BillingManager(Activity activity) { + mActivity = activity; + mBillingClient = BillingClient.newBuilder(mActivity).enablePendingPurchases().setListener(this).build(); + querySkuDetails(); + } + + static public boolean isPremiumCached() { + return mPreferences.getBoolean(SettingsFile.KEY_PREMIUM, false); + } + + /** + * @return true if Premium subscription is currently active + */ + public boolean isPremiumActive() { + return mIsPremiumActive; + } + + /** + * Invokes the billing flow for Premium + * + * @param callback Optional callback, called once, on completion of billing + */ + public void invokePremiumBilling(Runnable callback) { + if (mSkuPremium == null) { + return; + } + + // Optional callback to refresh the UI for the caller when billing completes + mUpdateBillingCallback = callback; + + // Invoke the billing flow + BillingFlowParams flowParams = BillingFlowParams.newBuilder() + .setSkuDetails(mSkuPremium) + .build(); + mBillingClient.launchBillingFlow(mActivity, flowParams); + } + + private void updatePremiumState(boolean isPremiumActive) { + mIsPremiumActive = isPremiumActive; + + // Cache state for synchronous UI + SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean(SettingsFile.KEY_PREMIUM, isPremiumActive); + editor.apply(); + + // No need to show button in action bar if Premium is active + MainActivity.setPremiumButtonVisible(!isPremiumActive); + } + + @Override + public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchaseList) { + if (purchaseList == null || purchaseList.isEmpty()) { + // Premium is not active, or billing is unavailable + updatePremiumState(false); + return; + } + + Purchase premiumPurchase = null; + for (Purchase purchase : purchaseList) { + if (purchase.getSku().equals(BILLING_SKU_PREMIUM)) { + premiumPurchase = purchase; + } + } + + if (premiumPurchase != null && premiumPurchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { + // Premium has been purchased + updatePremiumState(true); + + // Acknowledge the purchase if it hasn't already been acknowledged. + if (!premiumPurchase.isAcknowledged()) { + AcknowledgePurchaseParams acknowledgePurchaseParams = + AcknowledgePurchaseParams.newBuilder() + .setPurchaseToken(premiumPurchase.getPurchaseToken()) + .build(); + + AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = billingResult1 -> { + Toast.makeText(mActivity, R.string.premium_settings_welcome, Toast.LENGTH_SHORT).show(); + }; + mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener); + } + + if (mUpdateBillingCallback != null) { + try { + mUpdateBillingCallback.run(); + } catch (Exception e) { + e.printStackTrace(); + } + mUpdateBillingCallback = null; + } + } + } + + private void onQuerySkuDetailsFinished(List<SkuDetails> skuDetailsList) { + if (skuDetailsList == null) { + // This can happen when no user is signed in + return; + } + + if (skuDetailsList.isEmpty()) { + return; + } + + mSkuPremium = skuDetailsList.get(0); + + queryPurchases(); + } + + private void querySkuDetails() { + Runnable queryToExecute = new Runnable() { + @Override + public void run() { + SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder(); + List<String> skuList = new ArrayList<>(); + + skuList.add(BILLING_SKU_PREMIUM); + params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP); + + mBillingClient.querySkuDetailsAsync(params.build(), + (billingResult, skuDetailsList) -> onQuerySkuDetailsFinished(skuDetailsList)); + } + }; + + executeServiceRequest(queryToExecute); + } + + private void onQueryPurchasesFinished(PurchasesResult result) { + // Have we been disposed of in the meantime? If so, or bad result code, then quit + if (mBillingClient == null || result.getResponseCode() != BillingClient.BillingResponseCode.OK) { + updatePremiumState(false); + return; + } + // Update the UI and purchases inventory with new list of purchases + onPurchasesUpdated(result.getBillingResult(), result.getPurchasesList()); + } + + private void queryPurchases() { + Runnable queryToExecute = new Runnable() { + @Override + public void run() { + final PurchasesResult purchasesResult = mBillingClient.queryPurchases(BillingClient.SkuType.INAPP); + onQueryPurchasesFinished(purchasesResult); + } + }; + + executeServiceRequest(queryToExecute); + } + + private void startServiceConnection(final Runnable executeOnFinish) { + mBillingClient.startConnection(new BillingClientStateListener() { + @Override + public void onBillingSetupFinished(BillingResult billingResult) { + if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { + mIsServiceConnected = true; + } + + if (executeOnFinish != null) { + executeOnFinish.run(); + } + } + + @Override + public void onBillingServiceDisconnected() { + mIsServiceConnected = false; + } + }); + } + + private void executeServiceRequest(Runnable runnable) { + if (mIsServiceConnected) { + runnable.run(); + } else { + // If billing service was disconnected, we try to reconnect 1 time. + startServiceConnection(runnable); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java new file mode 100644 index 000000000..f801a05f0 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/ControllerMappingHelper.java @@ -0,0 +1,66 @@ +package org.citra.citra_emu.utils; + +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; + +/** + * Some controllers have incorrect mappings. This class has special-case fixes for them. + */ +public class ControllerMappingHelper { + /** + * Some controllers report extra button presses that can be ignored. + */ + public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode) { + if (isDualShock4(inputDevice)) { + // The two analog triggers generate analog motion events as well as a keycode. + // We always prefer to use the analog values, so throw away the button press + return keyCode == KeyEvent.KEYCODE_BUTTON_L2 || keyCode == KeyEvent.KEYCODE_BUTTON_R2; + } + return false; + } + + /** + * Scale an axis to be zero-centered with a proper range. + */ + public float scaleAxis(InputDevice inputDevice, int axis, float value) { + if (isDualShock4(inputDevice)) { + // Android doesn't have correct mappings for this controller's triggers. It reports them + // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0] + // Scale them to properly zero-centered with a range of [0.0, 1.0]. + if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) { + return (value + 1) / 2.0f; + } + } else if (isXboxOneWireless(inputDevice)) { + // Same as the DualShock 4, the mappings are missing. + if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) { + return (value + 1) / 2.0f; + } + if (axis == MotionEvent.AXIS_GENERIC_1) { + // This axis is stuck at ~.5. Ignore it. + return 0.0f; + } + } else if (isMogaPro2Hid(inputDevice)) { + // This controller has a broken axis that reports a constant value. Ignore it. + if (axis == MotionEvent.AXIS_GENERIC_1) { + return 0.0f; + } + } + return value; + } + + private boolean isDualShock4(InputDevice inputDevice) { + // Sony DualShock 4 controller + return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc; + } + + private boolean isXboxOneWireless(InputDevice inputDevice) { + // Microsoft Xbox One controller + return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0; + } + + private boolean isMogaPro2Hid(InputDevice inputDevice) { + // Moga Pro 2 HID + return inputDevice.getVendorId() == 0x20d6 && inputDevice.getProductId() == 0x6271; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryInitialization.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryInitialization.java new file mode 100644 index 000000000..58e552f5e --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryInitialization.java @@ -0,0 +1,186 @@ +/** + * Copyright 2014 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.citra.citra_emu.utils; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Environment; +import android.preference.PreferenceManager; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.citra.citra_emu.NativeLibrary; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A service that spawns its own thread in order to copy several binary and shader files + * from the Citra APK to the external file system. + */ +public final class DirectoryInitialization { + public static final String BROADCAST_ACTION = "org.citra.citra_emu.BROADCAST"; + + public static final String EXTRA_STATE = "directoryState"; + private static volatile DirectoryInitializationState directoryState = null; + private static String userPath; + private static AtomicBoolean isCitraDirectoryInitializationRunning = new AtomicBoolean(false); + + public static void start(Context context) { + // Can take a few seconds to run, so don't block UI thread. + //noinspection TrivialFunctionalExpressionUsage + ((Runnable) () -> init(context)).run(); + } + + private static void init(Context context) { + if (!isCitraDirectoryInitializationRunning.compareAndSet(false, true)) + return; + + if (directoryState != DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED) { + if (PermissionsHandler.hasWriteAccess(context)) { + if (setCitraUserDirectory()) { + initializeInternalStorage(context); + NativeLibrary.CreateConfigFile(); + directoryState = DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED; + } else { + directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE; + } + } else { + directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED; + } + } + + isCitraDirectoryInitializationRunning.set(false); + sendBroadcastState(directoryState, context); + } + + private static void deleteDirectoryRecursively(File file) { + if (file.isDirectory()) { + for (File child : file.listFiles()) + deleteDirectoryRecursively(child); + } + file.delete(); + } + + public static boolean areCitraDirectoriesReady() { + return directoryState == DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED; + } + + public static String getUserDirectory() { + if (directoryState == null) { + throw new IllegalStateException("DirectoryInitialization has to run at least once!"); + } else if (isCitraDirectoryInitializationRunning.get()) { + throw new IllegalStateException( + "DirectoryInitialization has to finish running first!"); + } + return userPath; + } + + private static native void SetSysDirectory(String path); + + private static boolean setCitraUserDirectory() { + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { + File externalPath = Environment.getExternalStorageDirectory(); + if (externalPath != null) { + userPath = externalPath.getAbsolutePath() + "/citra-emu"; + Log.debug("[DirectoryInitialization] User Dir: " + userPath); + // NativeLibrary.SetUserDirectory(userPath); + return true; + } + + } + + return false; + } + + private static void initializeInternalStorage(Context context) { + File sysDirectory = new File(context.getFilesDir(), "Sys"); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + String revision = NativeLibrary.GetGitRevision(); + if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) { + // There is no extracted Sys directory, or there is a Sys directory from another + // version of Citra that might contain outdated files. Let's (re-)extract Sys. + deleteDirectoryRecursively(sysDirectory); + copyAssetFolder("Sys", sysDirectory, true, context); + + SharedPreferences.Editor editor = preferences.edit(); + editor.putString("sysDirectoryVersion", revision); + editor.apply(); + } + + // Let the native code know where the Sys directory is. + SetSysDirectory(sysDirectory.getPath()); + } + + private static void sendBroadcastState(DirectoryInitializationState state, Context context) { + Intent localIntent = + new Intent(BROADCAST_ACTION) + .putExtra(EXTRA_STATE, state); + LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent); + } + + private static void copyAsset(String asset, File output, Boolean overwrite, Context context) { + Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output); + + try { + if (!output.exists() || overwrite) { + InputStream in = context.getAssets().open(asset); + OutputStream out = new FileOutputStream(output); + copyFile(in, out); + in.close(); + out.close(); + } + } catch (IOException e) { + Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset + + e.getMessage()); + } + } + + private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite, + Context context) { + Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " + + outputFolder); + + try { + boolean createdFolder = false; + for (String file : context.getAssets().list(assetFolder)) { + if (!createdFolder) { + outputFolder.mkdir(); + createdFolder = true; + } + copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file), + overwrite, context); + copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite, + context); + } + } catch (IOException e) { + Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder + + e.getMessage()); + } + } + + private static void copyFile(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024]; + int read; + + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + } + + public enum DirectoryInitializationState { + CITRA_DIRECTORIES_INITIALIZED, + EXTERNAL_STORAGE_PERMISSION_NEEDED, + CANT_FIND_EXTERNAL_STORAGE + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryStateReceiver.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryStateReceiver.java new file mode 100644 index 000000000..5d1e951ca --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/DirectoryStateReceiver.java @@ -0,0 +1,22 @@ +package org.citra.citra_emu.utils; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState; + +public class DirectoryStateReceiver extends BroadcastReceiver { + Action1<DirectoryInitializationState> callback; + + public DirectoryStateReceiver(Action1<DirectoryInitializationState> callback) { + this.callback = callback; + } + + @Override + public void onReceive(Context context, Intent intent) { + DirectoryInitializationState state = (DirectoryInitializationState) intent + .getSerializableExtra(DirectoryInitialization.EXTRA_STATE); + callback.call(state); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java new file mode 100644 index 000000000..9664f8464 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/EmulationMenuSettings.java @@ -0,0 +1,78 @@ +package org.citra.citra_emu.utils; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.citra.citra_emu.CitraApplication; + +public class EmulationMenuSettings { + private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); + + // These must match what is defined in src/core/settings.h + public static final int LayoutOption_Default = 0; + public static final int LayoutOption_SingleScreen = 1; + public static final int LayoutOption_LargeScreen = 2; + public static final int LayoutOption_SideScreen = 3; + public static final int LayoutOption_MobilePortrait = 4; + public static final int LayoutOption_MobileLandscape = 5; + + public static boolean getJoystickRelCenter() { + return mPreferences.getBoolean("EmulationMenuSettings_JoystickRelCenter", true); + } + + public static void setJoystickRelCenter(boolean value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean("EmulationMenuSettings_JoystickRelCenter", value); + editor.apply(); + } + + public static boolean getDpadSlideEnable() { + return mPreferences.getBoolean("EmulationMenuSettings_DpadSlideEnable", true); + } + + public static void setDpadSlideEnable(boolean value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean("EmulationMenuSettings_DpadSlideEnable", value); + editor.apply(); + } + + public static int getLandscapeScreenLayout() { + return mPreferences.getInt("EmulationMenuSettings_LandscapeScreenLayout", LayoutOption_MobileLandscape); + } + + public static void setLandscapeScreenLayout(int value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putInt("EmulationMenuSettings_LandscapeScreenLayout", value); + editor.apply(); + } + + public static boolean getShowFps() { + return mPreferences.getBoolean("EmulationMenuSettings_ShowFps", false); + } + + public static void setShowFps(boolean value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean("EmulationMenuSettings_ShowFps", value); + editor.apply(); + } + + public static boolean getSwapScreens() { + return mPreferences.getBoolean("EmulationMenuSettings_SwapScreens", false); + } + + public static void setSwapScreens(boolean value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean("EmulationMenuSettings_SwapScreens", value); + editor.apply(); + } + + public static boolean getShowOverlay() { + return mPreferences.getBoolean("EmulationMenuSettings_ShowOverylay", true); + } + + public static void setShowOverlay(boolean value) { + final SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean("EmulationMenuSettings_ShowOverylay", value); + editor.apply(); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java new file mode 100644 index 000000000..baf691f5c --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java @@ -0,0 +1,73 @@ +package org.citra.citra_emu.utils; + +import android.content.Intent; +import android.net.Uri; +import android.os.Environment; + +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; + +import com.nononsenseapps.filepicker.FilePickerActivity; +import com.nononsenseapps.filepicker.Utils; + +import org.citra.citra_emu.activities.CustomFilePickerActivity; + +import java.io.File; +import java.util.List; + +public final class FileBrowserHelper { + public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) { + Intent i = new Intent(activity, CustomFilePickerActivity.class); + + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR); + i.putExtra(FilePickerActivity.EXTRA_START_PATH, + Environment.getExternalStorageDirectory().getPath()); + i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); + i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); + + activity.startActivityForResult(i, requestCode); + } + + public static void openFilePicker(FragmentActivity activity, int requestCode, int title, + List<String> extensions, boolean allowMultiple) { + Intent i = new Intent(activity, CustomFilePickerActivity.class); + + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMultiple); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + i.putExtra(FilePickerActivity.EXTRA_START_PATH, + Environment.getExternalStorageDirectory().getPath()); + i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); + i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); + + activity.startActivityForResult(i, requestCode); + } + + @Nullable + public static String getSelectedDirectory(Intent result) { + // Use the provided utility method to parse the result + List<Uri> files = Utils.getSelectedFilesFromResult(result); + if (!files.isEmpty()) { + File file = Utils.getFileForUri(files.get(0)); + return file.getAbsolutePath(); + } + + return null; + } + + @Nullable + public static String[] getSelectedFiles(Intent result) { + // Use the provided utility method to parse the result + List<Uri> files = Utils.getSelectedFilesFromResult(result); + if (!files.isEmpty()) { + String[] paths = new String[files.size()]; + for (int i = 0; i < files.size(); i++) + paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath(); + return paths; + } + + return null; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java new file mode 100644 index 000000000..f9025171b --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.java @@ -0,0 +1,37 @@ +package org.citra.citra_emu.utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class FileUtil { + public static byte[] getBytesFromFile(File file) throws IOException { + final long length = file.length(); + + // You cannot create an array using a long type. + if (length > Integer.MAX_VALUE) { + // File is too large + throw new IOException("File is too large!"); + } + + byte[] bytes = new byte[(int) length]; + + int offset = 0; + int numRead; + + try (InputStream is = new FileInputStream(file)) { + while (offset < bytes.length + && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { + offset += numRead; + } + } + + // Ensure all the bytes have been read in + if (offset < bytes.length) { + throw new IOException("Could not completely read file " + file.getName()); + } + + return bytes; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java new file mode 100644 index 000000000..bc256877b --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/ForegroundService.java @@ -0,0 +1,63 @@ +/** + * Copyright 2014 Dolphin Emulator Project + * Licensed under GPLv2+ + * Refer to the license.txt file included. + */ + +package org.citra.citra_emu.utils; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.activities.EmulationActivity; + +/** + * A service that shows a permanent notification in the background to avoid the app getting + * cleared from memory by the system. + */ +public class ForegroundService extends Service { + private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000; + + private void showRunningNotification() { + // Intent is used to resume emulation if the notification is clicked + PendingIntent contentIntent = PendingIntent.getActivity(this, 0, + new Intent(this, EmulationActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.app_notification_channel_id)) + .setSmallIcon(R.drawable.ic_stat_notification_logo) + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.app_notification_running)) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setOngoing(true) + .setVibrate(null) + .setSound(null) + .setContentIntent(contentIntent); + startForeground(EMULATION_RUNNING_NOTIFICATION, builder.build()); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + showRunningNotification(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return START_STICKY; + } + + @Override + public void onDestroy() { + NotificationManagerCompat.from(this).cancel(EMULATION_RUNNING_NOTIFICATION); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java new file mode 100644 index 000000000..b790c2480 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameIconRequestHandler.java @@ -0,0 +1,27 @@ +package org.citra.citra_emu.utils; + +import android.graphics.Bitmap; + +import com.squareup.picasso.Picasso; +import com.squareup.picasso.Request; +import com.squareup.picasso.RequestHandler; + +import org.citra.citra_emu.NativeLibrary; + +import java.nio.IntBuffer; + +public class GameIconRequestHandler extends RequestHandler { + @Override + public boolean canHandleRequest(Request data) { + return "iso".equals(data.uri.getScheme()); + } + + @Override + public Result load(Request request, int networkPolicy) { + String url = request.uri.getHost() + request.uri.getPath(); + int[] vector = NativeLibrary.GetIcon(url); + Bitmap bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.RGB_565); + bitmap.copyPixelsFromBuffer(IntBuffer.wrap(vector)); + return new Result(bitmap, Picasso.LoadedFrom.DISK); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java new file mode 100644 index 000000000..070d01eb1 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java @@ -0,0 +1,39 @@ +package org.citra.citra_emu.utils; + +import org.citra.citra_emu.BuildConfig; + +/** + * Contains methods that call through to {@link android.util.Log}, but + * with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log + * levels in release builds. + */ +public final class Log { + private static final String TAG = "Citra Frontend"; + + private Log() { + } + + public static void verbose(String message) { + if (BuildConfig.DEBUG) { + android.util.Log.v(TAG, message); + } + } + + public static void debug(String message) { + if (BuildConfig.DEBUG) { + android.util.Log.d(TAG, message); + } + } + + public static void info(String message) { + android.util.Log.i(TAG, message); + } + + public static void warning(String message) { + android.util.Log.w(TAG, message); + } + + public static void error(String message) { + android.util.Log.e(TAG, message); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.java new file mode 100644 index 000000000..a29e23e8d --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/PermissionsHandler.java @@ -0,0 +1,35 @@ +package org.citra.citra_emu.utils; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; + +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; + +import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; + +public class PermissionsHandler { + public static final int REQUEST_CODE_WRITE_PERMISSION = 500; + + // We use permissions acceptance as an indicator if this is a first boot for the user. + public static boolean isFirstBoot(final FragmentActivity activity) { + return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED; + } + + @TargetApi(Build.VERSION_CODES.M) + public static boolean checkWritePermission(final FragmentActivity activity) { + if (isFirstBoot(activity)) { + activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, + REQUEST_CODE_WRITE_PERMISSION); + return false; + } + + return true; + } + + public static boolean hasWriteAccess(Context context) { + return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoRoundedCornersTransformation.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoRoundedCornersTransformation.java new file mode 100644 index 000000000..892b46387 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoRoundedCornersTransformation.java @@ -0,0 +1,45 @@ +package org.citra.citra_emu.utils; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; + +import com.squareup.picasso.Transformation; + +public class PicassoRoundedCornersTransformation implements Transformation { + @Override + public Bitmap transform(Bitmap icon) { + final int width = icon.getWidth(); + final int height = icon.getHeight(); + final Rect rect = new Rect(0, 0, width, height); + final int size = Math.min(width, height); + final int x = (width - size) / 2; + final int y = (height - size) / 2; + + Bitmap squaredBitmap = Bitmap.createBitmap(icon, x, y, size, size); + if (squaredBitmap != icon) { + icon.recycle(); + } + + Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setShader(shader); + + canvas.drawRoundRect(new RectF(rect), 10, 10, paint); + + squaredBitmap.recycle(); + + return output; + } + + @Override + public String key() { + return "circle"; + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java new file mode 100644 index 000000000..c99726685 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java @@ -0,0 +1,57 @@ +package org.citra.citra_emu.utils; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.widget.ImageView; + +import com.squareup.picasso.Picasso; + +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.R; + +import java.io.IOException; + +import androidx.annotation.Nullable; + +public class PicassoUtils { + private static boolean mPicassoInitialized = false; + + public static void init() { + if (mPicassoInitialized) { + return; + } + Picasso picassoInstance = new Picasso.Builder(CitraApplication.getAppContext()) + .addRequestHandler(new GameIconRequestHandler()) + .build(); + + Picasso.setSingletonInstance(picassoInstance); + mPicassoInitialized = true; + } + + public static void loadGameIcon(ImageView imageView, String gamePath) { + Picasso + .get() + .load(Uri.parse("iso:/" + gamePath)) + .fit() + .centerInside() + .config(Bitmap.Config.RGB_565) + .error(R.drawable.no_icon) + .transform(new PicassoRoundedCornersTransformation()) + .into(imageView); + } + + // Blocking call. Load image from file and crop/resize it to fit in width x height. + @Nullable + public static Bitmap LoadBitmapFromFile(String uri, int width, int height) { + try { + return Picasso.get() + .load(Uri.parse(uri)) + .config(Bitmap.Config.ARGB_8888) + .centerCrop() + .resize(width, height) + .get(); + } catch (IOException e) { + return null; + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/StartupHandler.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/StartupHandler.java new file mode 100644 index 000000000..9112bf90c --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/StartupHandler.java @@ -0,0 +1,45 @@ +package org.citra.citra_emu.utils; + +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; + +import org.citra.citra_emu.R; +import org.citra.citra_emu.activities.EmulationActivity; + +public final class StartupHandler { + private static void handlePermissionsCheck(FragmentActivity parent) { + // Ask the user to grant write permission if it's not already granted + PermissionsHandler.checkWritePermission(parent); + + String start_file = ""; + Bundle extras = parent.getIntent().getExtras(); + if (extras != null) { + start_file = extras.getString("AutoStartFile"); + } + + if (!TextUtils.isEmpty(start_file)) { + // Start the emulation activity, send the ISO passed in and finish the main activity + Intent emulation_intent = new Intent(parent, EmulationActivity.class); + emulation_intent.putExtra("SelectedGame", start_file); + parent.startActivity(emulation_intent); + parent.finish(); + } + } + + public static void HandleInit(FragmentActivity parent) { + if (PermissionsHandler.isFirstBoot(parent)) { + // Prompt user with standard first boot disclaimer + new AlertDialog.Builder(parent) + .setTitle(R.string.app_name) + .setIcon(R.mipmap.ic_launcher) + .setMessage(parent.getResources().getString(R.string.app_disclaimer)) + .setPositiveButton(android.R.string.ok, null) + .setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent)) + .show(); + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.java new file mode 100644 index 000000000..74ef3867f --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.java @@ -0,0 +1,34 @@ +package org.citra.citra_emu.utils; + +import android.content.SharedPreferences; +import android.os.Build; +import android.preference.PreferenceManager; + +import androidx.appcompat.app.AppCompatDelegate; + +import org.citra.citra_emu.CitraApplication; +import org.citra.citra_emu.features.settings.utils.SettingsFile; + +public class ThemeUtil { + private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext()); + + private static void applyTheme(int designValue) { + switch (designValue) { + case 0: + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + break; + case 1: + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + break; + case 2: + AppCompatDelegate.setDefaultNightMode(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM : + AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY); + break; + } + } + + public static void applyTheme() { + applyTheme(mPreferences.getInt(SettingsFile.KEY_DESIGN, 0)); + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java new file mode 100644 index 000000000..50dbcbe18 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java @@ -0,0 +1,46 @@ +package org.citra.citra_emu.viewholders; + +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import org.citra.citra_emu.R; + +/** + * A simple class that stores references to views so that the GameAdapter doesn't need to + * keep calling findViewById(), which is expensive. + */ +public class GameViewHolder extends RecyclerView.ViewHolder { + private View itemView; + public ImageView imageIcon; + public TextView textGameTitle; + public TextView textCompany; + public TextView textFileName; + + public String gameId; + + // TODO Not need any of this stuff. Currently only the properties dialog needs it. + public String path; + public String title; + public String description; + public String regions; + public String company; + + public GameViewHolder(View itemView) { + super(itemView); + + this.itemView = itemView; + itemView.setTag(this); + + imageIcon = itemView.findViewById(R.id.image_game_screen); + textGameTitle = itemView.findViewById(R.id.text_game_title); + textCompany = itemView.findViewById(R.id.text_company); + textFileName = itemView.findViewById(R.id.text_filename); + } + + public View getItemView() { + return itemView; + } +} diff --git a/src/android/app/src/main/java/org/citra_emu/citra/CitraApplication.java b/src/android/app/src/main/java/org/citra_emu/citra/CitraApplication.java deleted file mode 100644 index 10cb52783..000000000 --- a/src/android/app/src/main/java/org/citra_emu/citra/CitraApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra_emu.citra; - -import android.app.Application; - -public class CitraApplication extends Application { - static { - System.loadLibrary("citra-android"); - } -} diff --git a/src/android/app/src/main/java/org/citra_emu/citra/LOG.java b/src/android/app/src/main/java/org/citra_emu/citra/LOG.java deleted file mode 100644 index c52f30b68..000000000 --- a/src/android/app/src/main/java/org/citra_emu/citra/LOG.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.citra_emu.citra; - -public class LOG { - - private interface LOG_LEVEL { - int TRACE = 0, DEBUG = 1, INFO = 2, WARNING = 3, ERROR = 4, CRITICAL = 5; - } - - public static void TRACE(String msg, Object... args) { - LOG(LOG_LEVEL.TRACE, msg, args); - } - - public static void DEBUG(String msg, Object... args) { - LOG(LOG_LEVEL.DEBUG, msg, args); - } - - public static void INFO(String msg, Object... args) { - LOG(LOG_LEVEL.INFO, msg, args); - } - - public static void WARNING(String msg, Object... args) { - LOG(LOG_LEVEL.WARNING, msg, args); - } - - public static void ERROR(String msg, Object... args) { - LOG(LOG_LEVEL.ERROR, msg, args); - } - - public static void CRITICAL(String msg, Object... args) { - LOG(LOG_LEVEL.CRITICAL, msg, args); - } - - private static void LOG(int level, String msg, Object... args) { - StackTraceElement trace = Thread.currentThread().getStackTrace()[4]; - logEntry(level, trace.getFileName(), trace.getLineNumber(), trace.getMethodName(), - String.format(msg, args)); - } - - private static native void logEntry(int level, String file_name, int line_number, - String function, String message); -} diff --git a/src/android/app/src/main/java/org/citra_emu/citra/ui/main/MainActivity.java b/src/android/app/src/main/java/org/citra_emu/citra/ui/main/MainActivity.java deleted file mode 100644 index 5b4f3d3bc..000000000 --- a/src/android/app/src/main/java/org/citra_emu/citra/ui/main/MainActivity.java +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2018 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra_emu.citra.ui.main; - -import android.Manifest; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; - -import org.citra_emu.citra.R; -import org.citra_emu.citra.utils.FileUtil; -import org.citra_emu.citra.utils.PermissionUtil; - -public final class MainActivity extends AppCompatActivity { - - // Java enums suck - private interface PermissionCodes { int INITIALIZE = 0; } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - PermissionUtil.verifyPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, - PermissionCodes.INITIALIZE); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { - switch (requestCode) { - case PermissionCodes.INITIALIZE: - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - initUserPath(FileUtil.getUserPath().toString()); - initLogging(); - } else { - AlertDialog.Builder dialog = - new AlertDialog.Builder(this) - .setTitle("Permission Error") - .setMessage("Citra requires storage permissions to function.") - .setCancelable(false) - .setPositiveButton("OK", (dialogInterface, which) -> { - PermissionUtil.verifyPermission( - MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE, - PermissionCodes.INITIALIZE); - }); - dialog.show(); - } - } - } - - private static native void initUserPath(String path); - private static native void initLogging(); -} diff --git a/src/android/app/src/main/java/org/citra_emu/citra/utils/FileUtil.java b/src/android/app/src/main/java/org/citra_emu/citra/utils/FileUtil.java deleted file mode 100644 index 5346c5352..000000000 --- a/src/android/app/src/main/java/org/citra_emu/citra/utils/FileUtil.java +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra_emu.citra.utils; - -import android.os.Environment; - -import java.io.File; - -public class FileUtil { - public static File getUserPath() { - File storage = Environment.getExternalStorageDirectory(); - File userPath = new File(storage, "citra"); - if (!userPath.isDirectory()) - userPath.mkdir(); - return userPath; - } -} diff --git a/src/android/app/src/main/java/org/citra_emu/citra/utils/PermissionUtil.java b/src/android/app/src/main/java/org/citra_emu/citra/utils/PermissionUtil.java deleted file mode 100644 index 33c8129e5..000000000 --- a/src/android/app/src/main/java/org/citra_emu/citra/utils/PermissionUtil.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2019 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra_emu.citra.utils; - -import android.app.Activity; -import android.content.pm.PackageManager; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; - -public class PermissionUtil { - - /** - * Checks a permission, if needed shows a dialog to request it - * - * @param activity the activity requiring the permission - * @param permission the permission needed - * @param requestCode supplied to the callback to determine the next action - */ - public static void verifyPermission(Activity activity, String permission, int requestCode) { - if (ContextCompat.checkSelfPermission(activity, permission) == - PackageManager.PERMISSION_GRANTED) { - // call the callback called by requestPermissions - activity.onRequestPermissionsResult(requestCode, new String[] {permission}, - new int[] {PackageManager.PERMISSION_GRANTED}); - return; - } - - ActivityCompat.requestPermissions(activity, new String[] {permission}, requestCode); - } -} diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt new file mode 100644 index 000000000..3fd3a3eab --- /dev/null +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -0,0 +1,34 @@ +add_library(main SHARED + applets/mii_selector.cpp + applets/mii_selector.h + applets/swkbd.cpp + applets/swkbd.h + input_manager.cpp + input_manager.h + camera/ndk_camera.cpp + camera/ndk_camera.h + camera/still_image_camera.cpp + camera/still_image_camera.h + config.cpp + config.h + default_ini.h + emu_window/emu_window.cpp + emu_window/emu_window.h + game_info.cpp + game_info.h + game_settings.cpp + game_settings.h + id_cache.cpp + id_cache.h + mic.cpp + mic.h + native.cpp + native.h + ndk_motion.cpp + ndk_motion.h +) + +target_link_libraries(main PRIVATE audio_core common core input_common network) +target_link_libraries(main PRIVATE android camera2ndk mediandk jnigraphics EGL glad inih log yuv) + +set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} main) diff --git a/src/android/app/src/main/jni/applets/mii_selector.cpp b/src/android/app/src/main/jni/applets/mii_selector.cpp new file mode 100644 index 000000000..0e8e79238 --- /dev/null +++ b/src/android/app/src/main/jni/applets/mii_selector.cpp @@ -0,0 +1,87 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/string_util.h" +#include "jni/applets/mii_selector.h" +#include "jni/id_cache.h" + +static jclass s_mii_selector_class; +static jclass s_mii_selector_config_class; +static jclass s_mii_selector_data_class; +static jmethodID s_mii_selector_execute; + +namespace MiiSelector { + +AndroidMiiSelector::~AndroidMiiSelector() = default; + +void AndroidMiiSelector::Setup(const Frontend::MiiSelectorConfig& config) { + JNIEnv* env = IDCache::GetEnvForThread(); + auto miis = Frontend::LoadMiis(); + + // Create the Java MiiSelectorConfig object + jobject java_config = env->AllocObject(s_mii_selector_config_class); + env->SetBooleanField(java_config, + env->GetFieldID(s_mii_selector_config_class, "enable_cancel_button", "Z"), + static_cast<jboolean>(config.enable_cancel_button)); + env->SetObjectField(java_config, + env->GetFieldID(s_mii_selector_config_class, "title", "Ljava/lang/String;"), + env->NewStringUTF(config.title.c_str())); + env->SetLongField( + java_config, + env->GetFieldID(s_mii_selector_config_class, "initially_selected_mii_index", "J"), + static_cast<jlong>(config.initially_selected_mii_index)); + + // List mii names + // The 'Standard Mii' is not included here as we need Java side to translate it + const jclass string_class = reinterpret_cast<jclass>(env->FindClass("java/lang/String")); + const jobjectArray array = + env->NewObjectArray(static_cast<jsize>(miis.size()), string_class, nullptr); + for (std::size_t i = 0; i < miis.size(); ++i) { + const auto name = Common::UTF16BufferToUTF8(miis[i].mii_name); + env->SetObjectArrayElement(array, static_cast<jsize>(i), env->NewStringUTF(name.c_str())); + } + env->SetObjectField( + java_config, + env->GetFieldID(s_mii_selector_config_class, "mii_names", "[Ljava/lang/String;"), array); + + // Invoke backend Execute method + jobject data = + env->CallStaticObjectMethod(s_mii_selector_class, s_mii_selector_execute, java_config); + + const u32 return_code = static_cast<u32>( + env->GetLongField(data, env->GetFieldID(s_mii_selector_data_class, "return_code", "J"))); + if (return_code == 1) { + Finalize(return_code, HLE::Applets::MiiData{}); + return; + } + + const int index = static_cast<int>( + env->GetIntField(data, env->GetFieldID(s_mii_selector_data_class, "index", "I"))); + ASSERT_MSG(index >= 0 && index <= miis.size(), "Index returned is out of bound"); + Finalize(return_code, index == 0 + ? HLE::Applets::MiiSelector::GetStandardMiiResult().selected_mii_data + : miis.at(static_cast<std::size_t>(index - 1))); +} + +void InitJNI(JNIEnv* env) { + s_mii_selector_class = reinterpret_cast<jclass>( + env->NewGlobalRef(env->FindClass("org/citra/citra_emu/applets/MiiSelector"))); + s_mii_selector_config_class = reinterpret_cast<jclass>(env->NewGlobalRef( + env->FindClass("org/citra/citra_emu/applets/MiiSelector$MiiSelectorConfig"))); + s_mii_selector_data_class = reinterpret_cast<jclass>(env->NewGlobalRef( + env->FindClass("org/citra/citra_emu/applets/MiiSelector$MiiSelectorData"))); + + s_mii_selector_execute = + env->GetStaticMethodID(s_mii_selector_class, "Execute", + "(Lorg/citra/citra_emu/applets/MiiSelector$MiiSelectorConfig;)Lorg/" + "citra/citra_emu/applets/MiiSelector$MiiSelectorData;"); +} + +void CleanupJNI(JNIEnv* env) { + env->DeleteGlobalRef(s_mii_selector_class); + env->DeleteGlobalRef(s_mii_selector_config_class); + env->DeleteGlobalRef(s_mii_selector_data_class); +} + +} // namespace MiiSelector diff --git a/src/android/app/src/main/jni/applets/mii_selector.h b/src/android/app/src/main/jni/applets/mii_selector.h new file mode 100644 index 000000000..f33d1cb8d --- /dev/null +++ b/src/android/app/src/main/jni/applets/mii_selector.h @@ -0,0 +1,25 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <jni.h> +#include "core/frontend/applets/mii_selector.h" + +namespace MiiSelector { + +class AndroidMiiSelector final : public Frontend::MiiSelector { +public: + ~AndroidMiiSelector(); + + void Setup(const Frontend::MiiSelectorConfig& config) override; +}; + +// Should be called in JNI_Load +void InitJNI(JNIEnv* env); + +// Should be called in JNI_Unload +void CleanupJNI(JNIEnv* env); + +} // namespace MiiSelector diff --git a/src/android/app/src/main/jni/applets/swkbd.cpp b/src/android/app/src/main/jni/applets/swkbd.cpp new file mode 100644 index 000000000..062d307a6 --- /dev/null +++ b/src/android/app/src/main/jni/applets/swkbd.cpp @@ -0,0 +1,151 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <map> +#include <jni.h> +#include "core/core.h" +#include "jni/applets/swkbd.h" +#include "jni/id_cache.h" + +static std::string GetJString(JNIEnv* env, jstring jstr) { + if (!jstr) { + return {}; + } + + const char* s = env->GetStringUTFChars(jstr, nullptr); + std::string result = s; + env->ReleaseStringUTFChars(jstr, s); + return result; +} + +static jclass s_software_keyboard_class; +static jclass s_keyboard_config_class; +static jclass s_keyboard_data_class; +static jclass s_validation_error_class; +static jmethodID s_swkbd_execute; +static jmethodID s_swkbd_show_error; + +namespace SoftwareKeyboard { + +static jobject ToJavaKeyboardConfig(const Frontend::KeyboardConfig& config) { + JNIEnv* env = IDCache::GetEnvForThread(); + jobject object = env->AllocObject(s_keyboard_config_class); + env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "button_config", "I"), + static_cast<jint>(config.button_config)); + env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "max_text_length", "I"), + static_cast<jint>(config.max_text_length)); + env->SetBooleanField(object, env->GetFieldID(s_keyboard_config_class, "multiline_mode", "Z"), + static_cast<jboolean>(config.multiline_mode)); + env->SetObjectField(object, + env->GetFieldID(s_keyboard_config_class, "hint_text", "Ljava/lang/String;"), + env->NewStringUTF(config.hint_text.c_str())); + + const jclass string_class = reinterpret_cast<jclass>(env->FindClass("java/lang/String")); + const jobjectArray array = + env->NewObjectArray(static_cast<jsize>(config.button_text.size()), string_class, + env->NewStringUTF(config.button_text[0].c_str())); + for (std::size_t i = 1; i < config.button_text.size(); ++i) { + env->SetObjectArrayElement(array, static_cast<jsize>(i), + env->NewStringUTF(config.button_text[i].c_str())); + } + env->SetObjectField( + object, env->GetFieldID(s_keyboard_config_class, "button_text", "[Ljava/lang/String;"), + array); + + return object; +} + +static Frontend::KeyboardData ToFrontendKeyboardData(jobject object) { + JNIEnv* env = IDCache::GetEnvForThread(); + const jstring string = reinterpret_cast<jstring>(env->GetObjectField( + object, env->GetFieldID(s_keyboard_data_class, "text", "Ljava/lang/String;"))); + return Frontend::KeyboardData{ + GetJString(env, string), + static_cast<u8>( + env->GetIntField(object, env->GetFieldID(s_keyboard_data_class, "button", "I")))}; +} + +AndroidKeyboard::~AndroidKeyboard() = default; + +void AndroidKeyboard::Execute(const Frontend::KeyboardConfig& config) { + SoftwareKeyboard::Execute(config); + + const auto data = ToFrontendKeyboardData(IDCache::GetEnvForThread()->CallStaticObjectMethod( + s_software_keyboard_class, s_swkbd_execute, ToJavaKeyboardConfig(config))); + Finalize(data.text, data.button); +} + +void AndroidKeyboard::ShowError(const std::string& error) { + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(s_software_keyboard_class, s_swkbd_show_error, + env->NewStringUTF(error.c_str())); +} + +void InitJNI(JNIEnv* env) { + s_software_keyboard_class = reinterpret_cast<jclass>( + env->NewGlobalRef(env->FindClass("org/citra/citra_emu/applets/SoftwareKeyboard"))); + s_keyboard_config_class = reinterpret_cast<jclass>(env->NewGlobalRef( + env->FindClass("org/citra/citra_emu/applets/SoftwareKeyboard$KeyboardConfig"))); + s_keyboard_data_class = reinterpret_cast<jclass>(env->NewGlobalRef( + env->FindClass("org/citra/citra_emu/applets/SoftwareKeyboard$KeyboardData"))); + s_validation_error_class = reinterpret_cast<jclass>(env->NewGlobalRef( + env->FindClass("org/citra/citra_emu/applets/SoftwareKeyboard$ValidationError"))); + + s_swkbd_execute = env->GetStaticMethodID( + s_software_keyboard_class, "Execute", + "(Lorg/citra/citra_emu/applets/SoftwareKeyboard$KeyboardConfig;)Lorg/citra/citra_emu/" + "applets/SoftwareKeyboard$KeyboardData;"); + s_swkbd_show_error = + env->GetStaticMethodID(s_software_keyboard_class, "ShowError", "(Ljava/lang/String;)V"); +} + +void CleanupJNI(JNIEnv* env) { + env->DeleteGlobalRef(s_software_keyboard_class); + env->DeleteGlobalRef(s_keyboard_config_class); + env->DeleteGlobalRef(s_keyboard_data_class); + env->DeleteGlobalRef(s_validation_error_class); +} + +} // namespace SoftwareKeyboard + +jobject ToJavaValidationError(Frontend::ValidationError error) { + static const std::map<Frontend::ValidationError, const char*> ValidationErrorNameMap{{ + {Frontend::ValidationError::None, "None"}, + {Frontend::ValidationError::ButtonOutOfRange, "ButtonOutOfRange"}, + {Frontend::ValidationError::MaxDigitsExceeded, "MaxDigitsExceeded"}, + {Frontend::ValidationError::AtSignNotAllowed, "AtSignNotAllowed"}, + {Frontend::ValidationError::PercentNotAllowed, "PercentNotAllowed"}, + {Frontend::ValidationError::BackslashNotAllowed, "BackslashNotAllowed"}, + {Frontend::ValidationError::ProfanityNotAllowed, "ProfanityNotAllowed"}, + {Frontend::ValidationError::CallbackFailed, "CallbackFailed"}, + {Frontend::ValidationError::FixedLengthRequired, "FixedLengthRequired"}, + {Frontend::ValidationError::MaxLengthExceeded, "MaxLengthExceeded"}, + {Frontend::ValidationError::BlankInputNotAllowed, "BlankInputNotAllowed"}, + {Frontend::ValidationError::EmptyInputNotAllowed, "EmptyInputNotAllowed"}, + }}; + ASSERT(ValidationErrorNameMap.count(error)); + + JNIEnv* env = IDCache::GetEnvForThread(); + return env->GetStaticObjectField( + s_validation_error_class, + env->GetStaticFieldID(s_validation_error_class, ValidationErrorNameMap.at(error), + "Lorg/citra/citra_emu/applets/SoftwareKeyboard$ValidationError;")); +} + +jobject Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateFilters(JNIEnv* env, + jclass clazz, + jstring text) { + + const auto ret = + Core::System::GetInstance().GetSoftwareKeyboard()->ValidateFilters(GetJString(env, text)); + return ToJavaValidationError(ret); +} + +jobject Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateInput(JNIEnv* env, jclass clazz, + jstring text) { + + const auto ret = + Core::System::GetInstance().GetSoftwareKeyboard()->ValidateInput(GetJString(env, text)); + return ToJavaValidationError(ret); +} diff --git a/src/android/app/src/main/jni/applets/swkbd.h b/src/android/app/src/main/jni/applets/swkbd.h new file mode 100644 index 000000000..664626695 --- /dev/null +++ b/src/android/app/src/main/jni/applets/swkbd.h @@ -0,0 +1,35 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <jni.h> +#include "core/frontend/applets/swkbd.h" + +namespace SoftwareKeyboard { + +class AndroidKeyboard final : public Frontend::SoftwareKeyboard { +public: + ~AndroidKeyboard(); + + void Execute(const Frontend::KeyboardConfig& config) override; + void ShowError(const std::string& error) override; +}; + +// Should be called in JNI_Load +void InitJNI(JNIEnv* env); + +// Should be called in JNI_Unload +void CleanupJNI(JNIEnv* env); + +} // namespace SoftwareKeyboard + +// Native function calls +extern "C" { +JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateFilters( + JNIEnv* env, jclass clazz, jstring text); + +JNIEXPORT jobject JNICALL Java_org_citra_citra_1emu_applets_SoftwareKeyboard_ValidateInput( + JNIEnv* env, jclass clazz, jstring text); +} diff --git a/src/android/app/src/main/jni/camera/ndk_camera.cpp b/src/android/app/src/main/jni/camera/ndk_camera.cpp new file mode 100644 index 000000000..bf8ad18c8 --- /dev/null +++ b/src/android/app/src/main/jni/camera/ndk_camera.cpp @@ -0,0 +1,528 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <mutex> +#include <camera/NdkCameraCaptureSession.h> +#include <camera/NdkCameraDevice.h> +#include <camera/NdkCameraManager.h> +#include <camera/NdkCameraMetadata.h> +#include <camera/NdkCaptureRequest.h> +#include <libyuv.h> +#include <media/NdkImageReader.h> +#include "common/scope_exit.h" +#include "common/thread.h" +#include "core/frontend/camera/blank_camera.h" +#include "jni/camera/ndk_camera.h" +#include "jni/camera/still_image_camera.h" +#include "jni/id_cache.h" + +namespace Camera::NDK { + +/** + * Implementation detail of NDK camera interface, holding a ton of different structs. + * As long as the object lives, the camera is opened and capturing image. To turn off the camera, + * one needs to destruct the object. + * The pixel format is 'Android 420' which can contain a variety of YUV420 formats. The exact + * format used can be determined by examing the 'pixel stride'. + */ +struct CaptureSession final { + explicit CaptureSession(ACameraManager* manager, const std::string& id) { + Load(manager, id); + } + + void Load(ACameraManager* manager, const std::string& id); + + std::pair<int, int> selected_resolution{}; + + ACameraDevice_StateCallbacks device_callbacks{}; + AImageReader_ImageListener listener{}; + ACameraCaptureSession_stateCallbacks session_callbacks{}; + std::array<ACaptureRequest*, 1> requests{}; + +#define MEMBER(type, name, func) \ + struct type##Deleter { \ + void operator()(type* ptr) { \ + type##_##func(ptr); \ + } \ + }; \ + std::unique_ptr<type, type##Deleter> name + + MEMBER(ACameraDevice, device, close); + MEMBER(AImageReader, image_reader, delete); + MEMBER(ANativeWindow, native_window, release); + + MEMBER(ACaptureSessionOutputContainer, outputs, free); + MEMBER(ACaptureSessionOutput, output, free); + MEMBER(ACameraOutputTarget, target, free); + MEMBER(ACaptureRequest, request, free); + + // Put session last to close the session before we destruct everything else + MEMBER(ACameraCaptureSession, session, close); +#undef MEMBER + + bool ready = false; + + std::mutex data_mutex; + + // Clang does not yet have shared_ptr to arrays support. Managed data are actually arrays. + std::array<std::shared_ptr<u8>, 3> data{}; // I420 format, planes are Y, U, V. + std::array<int, 3> row_stride{}; // Row stride for the planes. + int pixel_stride{}; // Pixel stride for the UV planes. + Common::Event active_event; // Signals that the session has become ready + + int sensor_orientation{}; // Sensor Orientation + bool facing_front{}; // Whether this camera faces front. Used for handling device rotation. + + std::mutex status_mutex; + bool disconnected{}; // Whether this device has been closed and should be reopened + bool reload_requested{}; +}; + +void OnDisconnected(void* context, ACameraDevice* device) { + LOG_WARNING(Service_CAM, "Camera device disconnected"); + + CaptureSession* that = reinterpret_cast<CaptureSession*>(context); + { + std::lock_guard lock{that->status_mutex}; + that->disconnected = true; + } +} + +static void OnError(void* context, ACameraDevice* device, int error) { + LOG_ERROR(Service_CAM, "Camera device error {}", error); +} + +#define MEDIA_CALL(func) \ + { \ + auto ret = func; \ + if (ret != AMEDIA_OK) { \ + LOG_ERROR(Service_CAM, "Call " #func " returned error {}", ret); \ + return; \ + } \ + } + +#define CAMERA_CALL(func) \ + { \ + auto ret = func; \ + if (ret != ACAMERA_OK) { \ + LOG_ERROR(Service_CAM, "Call " #func " returned error {}", ret); \ + return; \ + } \ + } + +void ImageCallback(void* context, AImageReader* reader) { + AImage* image{}; + MEDIA_CALL(AImageReader_acquireLatestImage(reader, &image)); + SCOPE_EXIT({ AImage_delete(image); }); + + std::array<std::shared_ptr<u8>, 3> data; + std::array<int, 3> row_stride; + for (const int plane : {0, 1, 2}) { + u8* ptr; + int size; + MEDIA_CALL(AImage_getPlaneData(image, plane, &ptr, &size)); + data[plane].reset(new u8[size], std::default_delete<u8[]>()); + std::memcpy(data[plane].get(), ptr, static_cast<std::size_t>(size)); + + MEDIA_CALL(AImage_getPlaneRowStride(image, plane, &row_stride[plane])); + } + + CaptureSession* that = reinterpret_cast<CaptureSession*>(context); + { + std::lock_guard lock{that->data_mutex}; + that->data = data; + that->row_stride = row_stride; + MEDIA_CALL(AImage_getPlanePixelStride(image, 1, &that->pixel_stride)); + } + { + std::lock_guard lock{that->status_mutex}; + if (!that->ready) { + that->active_event.Set(); // Mark the session as active + } + } +} + +#define CREATE(type, name, statement) \ + { \ + type* raw; \ + statement; \ + name.reset(raw); \ + } + +// We have to define these no-op callbacks +static void OnClosed(void* context, ACameraCaptureSession* session) {} +static void OnReady(void* context, ACameraCaptureSession* session) {} +static void OnActive(void* context, ACameraCaptureSession* session) {} + +void CaptureSession::Load(ACameraManager* manager, const std::string& id) { + { + std::lock_guard lock{status_mutex}; + ready = disconnected = reload_requested = false; + } + + device_callbacks = { + /*context*/ this, + /*onDisconnected*/ &OnDisconnected, + /*onError*/ &OnError, + }; + + CREATE(ACameraDevice, device, + CAMERA_CALL(ACameraManager_openCamera(manager, id.c_str(), &device_callbacks, &raw))); + + ACameraMetadata* metadata; + CAMERA_CALL(ACameraManager_getCameraCharacteristics(manager, id.c_str(), &metadata)); + + ACameraMetadata_const_entry entry; + CAMERA_CALL(ACameraMetadata_getConstEntry( + metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &entry)); + + // We select the minimum resolution larger than 640x640 if any, or the maximum resolution. + selected_resolution = {}; + for (std::size_t i = 0; i < entry.count; i += 4) { + // (format, width, height, input?) + if (entry.data.i32[i + 3] & ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS_INPUT) { + // This is an input stream + continue; + } + + int format = entry.data.i32[i + 0]; + if (format == AIMAGE_FORMAT_YUV_420_888) { + int width = entry.data.i32[i + 1]; + int height = entry.data.i32[i + 2]; + if (selected_resolution.first <= 640 || selected_resolution.second <= 640) { + // Selected resolution is not large enough + selected_resolution = std::max(selected_resolution, std::make_pair(width, height)); + } else if (width >= 640 && height >= 640) { + // Selected resolution and this one are both large enough + selected_resolution = std::min(selected_resolution, std::make_pair(width, height)); + } + } + } + + CAMERA_CALL(ACameraMetadata_getConstEntry(metadata, ACAMERA_SENSOR_ORIENTATION, &entry)); + sensor_orientation = entry.data.i32[0]; + + CAMERA_CALL(ACameraMetadata_getConstEntry(metadata, ACAMERA_LENS_FACING, &entry)); + if (entry.data.i32[0] == ACAMERA_LENS_FACING_FRONT) { + facing_front = true; + } + ACameraMetadata_free(metadata); + + if (selected_resolution == std::pair<int, int>{}) { + LOG_ERROR(Service_CAM, "Device does not support any YUV output format"); + return; + } + + CREATE(AImageReader, image_reader, + MEDIA_CALL(AImageReader_new(selected_resolution.first, selected_resolution.second, + AIMAGE_FORMAT_YUV_420_888, 4, &raw))); + + listener = { + /*context*/ this, + /*onImageAvailable*/ &ImageCallback, + }; + MEDIA_CALL(AImageReader_setImageListener(image_reader.get(), &listener)); + + CREATE(ANativeWindow, native_window, + MEDIA_CALL(AImageReader_getWindow(image_reader.get(), &raw))); + ANativeWindow_acquire(native_window.get()); + + CREATE(ACaptureSessionOutput, output, + CAMERA_CALL(ACaptureSessionOutput_create(native_window.get(), &raw))); + + CREATE(ACaptureSessionOutputContainer, outputs, + CAMERA_CALL(ACaptureSessionOutputContainer_create(&raw))); + CAMERA_CALL(ACaptureSessionOutputContainer_add(outputs.get(), output.get())); + + session_callbacks = { + /*context*/ nullptr, + /*onClosed*/ &OnClosed, + /*onReady*/ &OnReady, + /*onActive*/ &OnActive, + }; + CREATE(ACameraCaptureSession, session, + CAMERA_CALL(ACameraDevice_createCaptureSession(device.get(), outputs.get(), + &session_callbacks, &raw))); + CREATE(ACaptureRequest, request, + CAMERA_CALL(ACameraDevice_createCaptureRequest(device.get(), TEMPLATE_PREVIEW, &raw))); + + CREATE(ACameraOutputTarget, target, + CAMERA_CALL(ACameraOutputTarget_create(native_window.get(), &raw))); + CAMERA_CALL(ACaptureRequest_addTarget(request.get(), target.get())); + + requests = {request.get()}; + CAMERA_CALL(ACameraCaptureSession_setRepeatingRequest(session.get(), nullptr, 1, + requests.data(), nullptr)); + + // Wait until the first image comes + active_event.Wait(); + { + std::lock_guard lock{status_mutex}; + ready = true; + } +} + +#undef MEDIA_CALL +#undef CAMERA_CALL +#undef CREATE + +Interface::Interface(Factory& factory_, const std::string& id_, const Service::CAM::Flip& flip) + : factory(factory_), id(id_) { + mirror = base_mirror = + flip == Service::CAM::Flip::Horizontal || flip == Service::CAM::Flip::Reverse; + invert = base_invert = + flip == Service::CAM::Flip::Vertical || flip == Service::CAM::Flip::Reverse; +} + +Interface::~Interface() { + factory.camera_permission_requested = false; +} + +void Interface::StartCapture() { + session = factory.CreateCaptureSession(id); +} + +void Interface::StopCapture() { + session.reset(); +} + +void Interface::SetResolution(const Service::CAM::Resolution& resolution_) { + resolution = resolution_; +} + +void Interface::SetFlip(Service::CAM::Flip flip) { + mirror = base_mirror ^ + (flip == Service::CAM::Flip::Horizontal || flip == Service::CAM::Flip::Reverse); + invert = + base_invert ^ (flip == Service::CAM::Flip::Vertical || flip == Service::CAM::Flip::Reverse); +} + +void Interface::SetFormat(Service::CAM::OutputFormat format_) { + format = format_; +} + +struct YUVImage { + int width{}; + int height{}; + std::vector<u8> y; + std::vector<u8> u; + std::vector<u8> v; + + explicit YUVImage(int width_, int height_) + : width(width_), height(height_), y(static_cast<std::size_t>(width * height)), + u(static_cast<std::size_t>(width * height / 4)), + v(static_cast<std::size_t>(width * height / 4)) {} + + void Swap(YUVImage& other) { + y.swap(other.y); + u.swap(other.u); + v.swap(other.v); + std::swap(width, other.width); + std::swap(height, other.height); + } + + void Clear() { + y.clear(); + u.clear(); + v.clear(); + width = height = 0; + } +}; + +#define YUV(image) \ + image.y.data(), image.width, image.u.data(), image.width / 2, image.v.data(), image.width / 2 + +std::vector<u16> Interface::ReceiveFrame() { + bool should_reload = false; + { + std::lock_guard lock{session->status_mutex}; + if (session->reload_requested) { + session->reload_requested = false; + should_reload = session->disconnected; + } + } + if (should_reload) { + LOG_INFO(Service_CAM, "Reloading camera session"); + session->Load(factory.manager.get(), id); + } + + bool session_ready; + { + std::lock_guard lock{session->status_mutex}; + session_ready = session->ready; + } + if (!session_ready) { // Camera was not opened + return std::vector<u16>(resolution.width * resolution.height); + } + + std::array<std::shared_ptr<u8>, 3> data; + std::array<int, 3> row_stride; + { + std::lock_guard lock{session->data_mutex}; + data = session->data; + row_stride = session->row_stride; + } + + ASSERT_MSG(data[0] && data[1] && data[2], "Data is not available"); + + auto [width, height] = session->selected_resolution; + + YUVImage converted(width, height); + libyuv::Android420ToI420(data[0].get(), row_stride[0], data[1].get(), row_stride[1], + data[2].get(), row_stride[2], session->pixel_stride, YUV(converted), + width, height); + + // Rotate the image to get it in upright position + // The g_rotation here is the display rotation which is opposite of the device rotation + const int rotation = + (session->sensor_orientation + (session->facing_front ? g_rotation : 4 - g_rotation) * 90) % + 360; + if (rotation == 90 || rotation == 270) { + std::swap(width, height); + } + YUVImage rotated(width, height); + libyuv::I420Rotate(YUV(converted), YUV(rotated), converted.width, converted.height, + static_cast<libyuv::RotationMode>(rotation)); + converted.Clear(); + + // Calculate crop coordinates + int crop_width{}, crop_height{}; + if (resolution.width * height > resolution.height * width) { + crop_width = width; + crop_height = width * resolution.height / resolution.width; + } else { + crop_height = height; + crop_width = height * resolution.width / resolution.height; + } + const int crop_x = (width - crop_width) / 2; + const int crop_y = (height - crop_height) / 2; + + const int y_offset = crop_y * width + crop_x; + const int uv_offset = crop_y / 2 * width / 2 + crop_x / 2; + YUVImage scaled(resolution.width, resolution.height); + // Crop and scale + libyuv::I420Scale(rotated.y.data() + y_offset, width, rotated.u.data() + uv_offset, width / 2, + rotated.v.data() + uv_offset, width / 2, crop_width, crop_height, YUV(scaled), + resolution.width, resolution.height, libyuv::kFilterBilinear); + rotated.Clear(); + + if (mirror) { + YUVImage mirrored(scaled.width, scaled.height); + libyuv::I420Mirror(YUV(scaled), YUV(mirrored), resolution.width, resolution.height); + scaled.Swap(mirrored); + } + + std::vector<u16> output(resolution.width * resolution.height); + if (format == Service::CAM::OutputFormat::RGB565) { + libyuv::I420ToRGB565(YUV(scaled), reinterpret_cast<u8*>(output.data()), + resolution.width * 2, resolution.width, + invert ? -resolution.height : resolution.height); + } else { + libyuv::I420ToYUY2(YUV(scaled), reinterpret_cast<u8*>(output.data()), resolution.width * 2, + resolution.width, invert ? -resolution.height : resolution.height); + } + return output; +} + +#undef YUV + +bool Interface::IsPreviewAvailable() { + if (!session) { + return false; + } + std::lock_guard lock{session->status_mutex}; + return session->ready; +} + +Factory::Factory() = default; + +Factory::~Factory() = default; + +std::shared_ptr<CaptureSession> Factory::CreateCaptureSession(const std::string& id) { + if (opened_camera_map.count(id) && !opened_camera_map.at(id).expired()) { + return opened_camera_map.at(id).lock(); + } + const auto& session = std::make_shared<CaptureSession>(manager.get(), id); + opened_camera_map.insert_or_assign(id, session); + return session; +} + +std::unique_ptr<CameraInterface> Factory::Create(const std::string& config, + const Service::CAM::Flip& flip) { + + manager.reset(ACameraManager_create()); + ACameraIdList* id_list = nullptr; + + auto ret = ACameraManager_getCameraIdList(manager.get(), &id_list); + if (ret != ACAMERA_OK) { + LOG_ERROR(Service_CAM, "Failed to get camera ID list: ret {}", ret); + return std::make_unique<Camera::BlankCamera>(); + } + + SCOPE_EXIT({ ACameraManager_deleteCameraIdList(id_list); }); + + if (id_list->numCameras <= 0) { + LOG_WARNING(Service_CAM, "No camera devices found, falling back to StillImage"); + // TODO: A better way of doing this? + return std::make_unique<StillImage::Factory>()->Create("", flip); + } + + // Request camera permission + if (!camera_permission_granted) { + if (camera_permission_requested) { // Permissions already denied + return std::make_unique<Camera::BlankCamera>(); + } + camera_permission_requested = true; + + JNIEnv* env = IDCache::GetEnvForThread(); + jboolean result = env->CallStaticBooleanMethod(IDCache::GetNativeLibraryClass(), + IDCache::GetRequestCameraPermission()); + if (result != JNI_TRUE) { + LOG_ERROR(Service_CAM, "Camera permissions denied"); + return std::make_unique<Camera::BlankCamera>(); + } + camera_permission_granted = true; + } + + if (config.empty()) { + LOG_WARNING(Service_CAM, "Camera ID not set, using default camera"); + return std::make_unique<Interface>(*this, id_list->cameraIds[0], flip); + } + + for (int i = 0; i < id_list->numCameras; ++i) { + const char* id = id_list->cameraIds[i]; + if (config == id) { + return std::make_unique<Interface>(*this, id, flip); + } + + if (config != FrontCameraPlaceholder && config != BackCameraPlaceholder) { + continue; + } + + ACameraMetadata* metadata; + ACameraManager_getCameraCharacteristics(manager.get(), id, &metadata); + SCOPE_EXIT({ ACameraMetadata_free(metadata); }); + + ACameraMetadata_const_entry entry; + ACameraMetadata_getConstEntry(metadata, ACAMERA_LENS_FACING, &entry); + if ((entry.data.i32[0] == ACAMERA_LENS_FACING_FRONT && config == FrontCameraPlaceholder) || + (entry.data.i32[0] == ACAMERA_LENS_FACING_BACK && config == BackCameraPlaceholder)) { + return std::make_unique<Interface>(*this, id, flip); + } + } + + LOG_ERROR(Service_CAM, "Camera ID {} not found", config); + return std::make_unique<Camera::BlankCamera>(); +} + +void Factory::ReloadCameraDevices() { + for (const auto& [id, ptr] : opened_camera_map) { + if (auto session = ptr.lock()) { + std::lock_guard lock{session->status_mutex}; + session->reload_requested = true; + } + } +} + +} // namespace Camera::NDK diff --git a/src/android/app/src/main/jni/camera/ndk_camera.h b/src/android/app/src/main/jni/camera/ndk_camera.h new file mode 100644 index 000000000..4b9bbfdaa --- /dev/null +++ b/src/android/app/src/main/jni/camera/ndk_camera.h @@ -0,0 +1,91 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <string_view> +#include <unordered_map> +#include <camera/NdkCameraManager.h> +#include "common/common_types.h" +#include "core/frontend/camera/factory.h" +#include "core/frontend/camera/interface.h" +#include "core/hle/service/cam/cam.h" + +namespace Camera::NDK { + +struct CaptureSession; +class Factory; + +class Interface : public CameraInterface { +public: + Interface(Factory& factory, const std::string& id, const Service::CAM::Flip& flip); + ~Interface() override; + void StartCapture() override; + void StopCapture() override; + void SetResolution(const Service::CAM::Resolution& resolution) override; + void SetFlip(Service::CAM::Flip flip) override; + void SetEffect(Service::CAM::Effect effect) override{}; + void SetFormat(Service::CAM::OutputFormat format) override; + void SetFrameRate(Service::CAM::FrameRate frame_rate) override{}; + std::vector<u16> ReceiveFrame() override; + bool IsPreviewAvailable() override; + +private: + Factory& factory; + std::shared_ptr<CaptureSession> session; + std::string id; + + Service::CAM::Resolution resolution; + + // Flipping parameters. mirror = horizontal, invert = vertical. + bool base_mirror{}; + bool base_invert{}; + bool mirror{}; + bool invert{}; + + Service::CAM::OutputFormat format; +}; + +// Placeholders to mean 'use any front/back camera' +constexpr std::string_view FrontCameraPlaceholder = "_front"; +constexpr std::string_view BackCameraPlaceholder = "_back"; + +class Factory final : public CameraFactory { +public: + explicit Factory(); + ~Factory() override; + + std::unique_ptr<CameraInterface> Create(const std::string& config, + const Service::CAM::Flip& flip) override; + + // Request the reopening of all previously disconnected camera devices. + // Called when the application is brought to foreground (i.e. we have priority with the camera) + void ReloadCameraDevices(); + +private: + // Avoid requesting for permission more than once on each call + bool camera_permission_requested = false; + bool camera_permission_granted = false; + + std::shared_ptr<CaptureSession> CreateCaptureSession(const std::string& id); + + // The session is cached, to avoid opening the same camera twice. + // This is weak_ptr so that the session is destructed when all cameras are closed + std::unordered_map<std::string, std::weak_ptr<CaptureSession>> opened_camera_map; + + struct ACameraManagerDeleter { + void operator()(ACameraManager* manager) { + ACameraManager_delete(manager); + } + }; + std::unique_ptr<ACameraManager, ACameraManagerDeleter> manager; + + friend class Interface; +}; + +// Device rotation. Updated in native.cpp. +inline int g_rotation = 0; + +} // namespace Camera::NDK diff --git a/src/android/app/src/main/jni/camera/still_image_camera.cpp b/src/android/app/src/main/jni/camera/still_image_camera.cpp new file mode 100644 index 000000000..496b51fa8 --- /dev/null +++ b/src/android/app/src/main/jni/camera/still_image_camera.cpp @@ -0,0 +1,143 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <android/bitmap.h> +#include <libyuv.h> +#include "common/logging/log.h" +#include "core/frontend/camera/blank_camera.h" +#include "jni/camera/still_image_camera.h" + +static jclass s_still_image_camera_helper_class; +static jmethodID s_open_file_picker; +static jmethodID s_load_image_from_file; + +namespace Camera::StillImage { + +void InitJNI(JNIEnv* env) { + s_still_image_camera_helper_class = reinterpret_cast<jclass>( + env->NewGlobalRef(env->FindClass("org/citra/citra_emu/camera/StillImageCameraHelper"))); + s_open_file_picker = env->GetStaticMethodID(s_still_image_camera_helper_class, "OpenFilePicker", + "()Ljava/lang/String;"); + s_load_image_from_file = + env->GetStaticMethodID(s_still_image_camera_helper_class, "LoadImageFromFile", + "(Ljava/lang/String;II)Landroid/graphics/Bitmap;"); +} + +void CleanupJNI(JNIEnv* env) { + env->DeleteGlobalRef(s_still_image_camera_helper_class); +} + +Interface::Interface(SharedGlobalRef<jstring> path_, const Service::CAM::Flip& flip) + : path(std::move(path_)) { + mirror = base_mirror = + flip == Service::CAM::Flip::Horizontal || flip == Service::CAM::Flip::Reverse; + invert = base_invert = + flip == Service::CAM::Flip::Vertical || flip == Service::CAM::Flip::Reverse; +} + +Interface::~Interface() { + Factory::last_path.reset(); +} + +void Interface::StartCapture() { + JNIEnv* env = IDCache::GetEnvForThread(); + jobject bitmap = + env->CallStaticObjectMethod(s_still_image_camera_helper_class, s_load_image_from_file, + path.get(), resolution.width, resolution.height); + if (bitmap == nullptr) { + LOG_ERROR(Frontend, "Could not load image from file"); + opened = false; + return; + } + + int ret; + +#define BITMAP_CALL(func) \ + ret = AndroidBitmap_##func; \ + if (ret != ANDROID_BITMAP_RESULT_SUCCESS) { \ + LOG_ERROR(Frontend, #func " failed with code {}", ret); \ + opened = false; \ + return; \ + } + + AndroidBitmapInfo info; + BITMAP_CALL(getInfo(env, bitmap, &info)); + ASSERT_MSG(info.format == AndroidBitmapFormat::ANDROID_BITMAP_FORMAT_RGBA_8888, + "Bitmap format was incorrect"); + ASSERT_MSG(info.width == resolution.width && info.height == resolution.height, + "Bitmap resolution was incorrect"); + + void* raw_data; + BITMAP_CALL(lockPixels(env, bitmap, &raw_data)); + std::vector<u8> data(info.height * info.stride); + libyuv::ABGRToARGB(reinterpret_cast<u8*>(raw_data), info.stride, data.data(), info.stride, + info.width, info.height); + BITMAP_CALL(unlockPixels(env, bitmap)); + + if (mirror) { + std::vector<u8> mirrored(data.size()); + libyuv::ARGBMirror(data.data(), info.stride, mirrored.data(), info.stride, info.width, + info.height); + data.swap(mirrored); + } + + image.resize(info.height * info.width); + if (format == Service::CAM::OutputFormat::RGB565) { + libyuv::ARGBToRGB565(data.data(), info.stride, reinterpret_cast<u8*>(image.data()), + info.width * 2, info.width, invert ? -info.height : info.height); + } else { + libyuv::ARGBToYUY2(data.data(), info.stride, reinterpret_cast<u8*>(image.data()), + info.width * 2, info.width, invert ? -info.height : info.height); + } + opened = true; + +#undef BITMAP_CALL +} + +void Interface::SetResolution(const Service::CAM::Resolution& resolution_) { + resolution = resolution_; +} + +void Interface::SetFlip(Service::CAM::Flip flip) { + mirror = base_mirror ^ + (flip == Service::CAM::Flip::Horizontal || flip == Service::CAM::Flip::Reverse); + invert = + base_invert ^ (flip == Service::CAM::Flip::Vertical || flip == Service::CAM::Flip::Reverse); +} + +void Interface::SetFormat(Service::CAM::OutputFormat format_) { + format = format_; +} + +std::vector<u16> Interface::ReceiveFrame() { + return image; +} + +bool Interface::IsPreviewAvailable() { + return opened; +} + +SharedGlobalRef<jstring> Factory::last_path{}; + +std::unique_ptr<CameraInterface> Factory::Create(const std::string& config, + const Service::CAM::Flip& flip) { + + JNIEnv* env = IDCache::GetEnvForThread(); + if (last_path != nullptr) { + return std::make_unique<Interface>(last_path, flip); + } + + // Open file picker to get the string + jstring path = reinterpret_cast<jstring>( + env->CallStaticObjectMethod(s_still_image_camera_helper_class, s_open_file_picker)); + if (path == nullptr) { + return std::make_unique<Camera::BlankCamera>(); + } else { + auto shared_path = NewSharedGlobalRef(path); + last_path = shared_path; + return std::make_unique<Interface>(std::move(shared_path), flip); + } +} + +} // namespace Camera::StillImage diff --git a/src/android/app/src/main/jni/camera/still_image_camera.h b/src/android/app/src/main/jni/camera/still_image_camera.h new file mode 100644 index 000000000..ab12040a3 --- /dev/null +++ b/src/android/app/src/main/jni/camera/still_image_camera.h @@ -0,0 +1,63 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <string> +#include <vector> +#include <jni.h> +#include "common/common_types.h" +#include "core/frontend/camera/factory.h" +#include "core/frontend/camera/interface.h" +#include "core/hle/service/cam/cam.h" +#include "jni/id_cache.h" + +namespace Camera::StillImage { + +class Interface final : public CameraInterface { +public: + Interface(SharedGlobalRef<jstring> path, const Service::CAM::Flip& flip); + ~Interface(); + void StartCapture() override; + void StopCapture() override{}; + void SetResolution(const Service::CAM::Resolution& resolution) override; + void SetFlip(Service::CAM::Flip flip) override; + void SetEffect(Service::CAM::Effect effect) override{}; + void SetFormat(Service::CAM::OutputFormat format) override; + void SetFrameRate(Service::CAM::FrameRate frame_rate) override{}; + std::vector<u16> ReceiveFrame() override; + bool IsPreviewAvailable() override; + +private: + SharedGlobalRef<jstring> path; + Service::CAM::Resolution resolution; + + // Flipping parameters. mirror = horizontal, invert = vertical. + bool base_mirror{}; + bool base_invert{}; + bool mirror{}; + bool invert{}; + + Service::CAM::OutputFormat format; + std::vector<u16> image; // Data fetched from the frontend + bool opened{}; // Whether the camera was successfully opened +}; + +class Factory final : public CameraFactory { +public: + std::unique_ptr<CameraInterface> Create(const std::string& config, + const Service::CAM::Flip& flip) override; + +private: + /// Record the path chosen to avoid multiple prompt problem + static SharedGlobalRef<jstring> last_path; + + friend class Interface; +}; + +void InitJNI(JNIEnv* env); +void CleanupJNI(JNIEnv* env); + +} // namespace Camera::StillImage diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp new file mode 100644 index 000000000..e8653da23 --- /dev/null +++ b/src/android/app/src/main/jni/config.cpp @@ -0,0 +1,279 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <iomanip> +#include <memory> +#include <sstream> +#include <unordered_map> +#include <inih/cpp/INIReader.h> + +#include "common/file_util.h" +#include "common/logging/log.h" +#include "common/param_package.h" +#include "core/core.h" +#include "core/hle/service/cfg/cfg.h" +#include "core/hle/service/service.h" +#include "core/settings.h" +#include "input_common/main.h" +#include "input_common/udp/client.h" +#include "jni/camera/ndk_camera.h" +#include "jni/config.h" +#include "jni/default_ini.h" +#include "jni/input_manager.h" + +Config::Config() { + // TODO: Don't hardcode the path; let the frontend decide where to put the config files. + sdl2_config_loc = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "config.ini"; + sdl2_config = std::make_unique<INIReader>(sdl2_config_loc); + + Reload(); +} + +Config::~Config() = default; + +bool Config::LoadINI(const std::string& default_contents, bool retry) { + const std::string& location = this->sdl2_config_loc; + if (sdl2_config->ParseError() < 0) { + if (retry) { + LOG_WARNING(Config, "Failed to load {}. Creating file from defaults...", location); + FileUtil::CreateFullPath(location); + FileUtil::WriteStringToFile(true, location, default_contents); + sdl2_config = std::make_unique<INIReader>(location); // Reopen file + + return LoadINI(default_contents, false); + } + LOG_ERROR(Config, "Failed."); + return false; + } + LOG_INFO(Config, "Successfully loaded {}", location); + return true; +} + +static const std::array<int, Settings::NativeButton::NumButtons> default_buttons = { + InputManager::N3DS_BUTTON_A, InputManager::N3DS_BUTTON_B, + InputManager::N3DS_BUTTON_X, InputManager::N3DS_BUTTON_Y, + InputManager::N3DS_DPAD_UP, InputManager::N3DS_DPAD_DOWN, + InputManager::N3DS_DPAD_LEFT, InputManager::N3DS_DPAD_RIGHT, + InputManager::N3DS_TRIGGER_L, InputManager::N3DS_TRIGGER_R, + InputManager::N3DS_BUTTON_START, InputManager::N3DS_BUTTON_SELECT, + InputManager::N3DS_BUTTON_DEBUG, InputManager::N3DS_BUTTON_GPIO14, + InputManager::N3DS_BUTTON_ZL, InputManager::N3DS_BUTTON_ZR, + InputManager::N3DS_BUTTON_HOME, +}; + +static const std::array<int, Settings::NativeAnalog::NumAnalogs> default_analogs{{ + InputManager::N3DS_CIRCLEPAD, + InputManager::N3DS_STICK_C, +}}; + +void Config::UpdateCFG() { + std::shared_ptr<Service::CFG::Module> cfg = std::make_shared<Service::CFG::Module>(); + cfg->SetSystemLanguage(static_cast<Service::CFG::SystemLanguage>( + sdl2_config->GetInteger("System", "language", Service::CFG::SystemLanguage::LANGUAGE_EN))); + cfg->UpdateConfigNANDSavegame(); +} + +void Config::ReadValues() { + // Controls + for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { + std::string default_param = InputManager::GenerateButtonParamPackage(default_buttons[i]); + Settings::values.current_input_profile.buttons[i] = + sdl2_config->GetString("Controls", Settings::NativeButton::mapping[i], default_param); + if (Settings::values.current_input_profile.buttons[i].empty()) + Settings::values.current_input_profile.buttons[i] = default_param; + } + + for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) { + std::string default_param = InputManager::GenerateAnalogParamPackage(default_analogs[i]); + Settings::values.current_input_profile.analogs[i] = + sdl2_config->GetString("Controls", Settings::NativeAnalog::mapping[i], default_param); + if (Settings::values.current_input_profile.analogs[i].empty()) + Settings::values.current_input_profile.analogs[i] = default_param; + } + + Settings::values.current_input_profile.motion_device = sdl2_config->GetString( + "Controls", "motion_device", + "engine:motion_emu,update_period:100,sensitivity:0.01,tilt_clamp:90.0"); + Settings::values.current_input_profile.touch_device = + sdl2_config->GetString("Controls", "touch_device", "engine:emu_window"); + Settings::values.current_input_profile.udp_input_address = sdl2_config->GetString( + "Controls", "udp_input_address", InputCommon::CemuhookUDP::DEFAULT_ADDR); + Settings::values.current_input_profile.udp_input_port = + static_cast<u16>(sdl2_config->GetInteger("Controls", "udp_input_port", + InputCommon::CemuhookUDP::DEFAULT_PORT)); + + // Core + Settings::values.use_cpu_jit = sdl2_config->GetBoolean("Core", "use_cpu_jit", true); + Settings::values.cpu_clock_percentage = + static_cast<int>(sdl2_config->GetInteger("Core", "cpu_clock_percentage", 100)); + + // Premium + Settings::values.texture_filter_name = + sdl2_config->GetString("Premium", "texture_filter_name", "none"); + + // Renderer + Settings::values.use_gles = sdl2_config->GetBoolean("Renderer", "use_gles", true); + Settings::values.use_hw_renderer = sdl2_config->GetBoolean("Renderer", "use_hw_renderer", true); + Settings::values.use_hw_shader = sdl2_config->GetBoolean("Renderer", "use_hw_shader", true); + Settings::values.shaders_accurate_mul = + sdl2_config->GetBoolean("Renderer", "shaders_accurate_mul", false); + Settings::values.use_asynchronous_gpu_emulation = + sdl2_config->GetBoolean("Renderer", "use_asynchronous_gpu_emulation", true); + Settings::values.use_shader_jit = sdl2_config->GetBoolean("Renderer", "use_shader_jit", true); + Settings::values.resolution_factor = + static_cast<u16>(sdl2_config->GetInteger("Renderer", "resolution_factor", 1)); + Settings::values.use_disk_shader_cache = + sdl2_config->GetBoolean("Renderer", "use_disk_shader_cache", true); + Settings::values.use_vsync_new = sdl2_config->GetBoolean("Renderer", "use_vsync_new", true); + + // Work around to map Android setting for enabling the frame limiter to the format Citra expects + if (sdl2_config->GetBoolean("Renderer", "use_frame_limit", true)) { + Settings::values.frame_limit = + static_cast<u16>(sdl2_config->GetInteger("Renderer", "frame_limit", 100)); + } else { + Settings::values.frame_limit = 0; + } + + Settings::values.render_3d = static_cast<Settings::StereoRenderOption>( + sdl2_config->GetInteger("Renderer", "render_3d", 0)); + Settings::values.factor_3d = + static_cast<u8>(sdl2_config->GetInteger("Renderer", "factor_3d", 0)); + std::string default_shader = "none (builtin)"; + if (Settings::values.render_3d == Settings::StereoRenderOption::Anaglyph) + default_shader = "dubois (builtin)"; + else if (Settings::values.render_3d == Settings::StereoRenderOption::Interlaced) + default_shader = "horizontal (builtin)"; + Settings::values.pp_shader_name = + sdl2_config->GetString("Renderer", "pp_shader_name", default_shader); + Settings::values.filter_mode = sdl2_config->GetBoolean("Renderer", "filter_mode", true); + + Settings::values.bg_red = static_cast<float>(sdl2_config->GetReal("Renderer", "bg_red", 0.0)); + Settings::values.bg_green = + static_cast<float>(sdl2_config->GetReal("Renderer", "bg_green", 0.0)); + Settings::values.bg_blue = static_cast<float>(sdl2_config->GetReal("Renderer", "bg_blue", 0.0)); + + // Layout + Settings::values.layout_option = static_cast<Settings::LayoutOption>(sdl2_config->GetInteger( + "Layout", "layout_option", static_cast<int>(Settings::LayoutOption::MobileLandscape))); + Settings::values.custom_layout = sdl2_config->GetBoolean("Layout", "custom_layout", false); + Settings::values.custom_top_left = + static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_top_left", 0)); + Settings::values.custom_top_top = + static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_top_top", 0)); + Settings::values.custom_top_right = + static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_top_right", 400)); + Settings::values.custom_top_bottom = + static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_top_bottom", 240)); + Settings::values.custom_bottom_left = + static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_bottom_left", 40)); + Settings::values.custom_bottom_top = + static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_bottom_top", 240)); + Settings::values.custom_bottom_right = + static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_bottom_right", 360)); + Settings::values.custom_bottom_bottom = + static_cast<u16>(sdl2_config->GetInteger("Layout", "custom_bottom_bottom", 480)); + Settings::values.cardboard_screen_size = + static_cast<int>(sdl2_config->GetInteger("Layout", "cardboard_screen_size", 85)); + Settings::values.cardboard_x_shift = + static_cast<int>(sdl2_config->GetInteger("Layout", "cardboard_x_shift", 0)); + Settings::values.cardboard_y_shift = + static_cast<int>(sdl2_config->GetInteger("Layout", "cardboard_y_shift", 0)); + + // Audio + Settings::values.enable_dsp_lle = sdl2_config->GetBoolean("Audio", "enable_dsp_lle", false); + Settings::values.enable_dsp_lle_multithread = + sdl2_config->GetBoolean("Audio", "enable_dsp_lle_multithread", false); + Settings::values.sink_id = sdl2_config->GetString("Audio", "output_engine", "auto"); + Settings::values.enable_audio_stretching = + sdl2_config->GetBoolean("Audio", "enable_audio_stretching", true); + Settings::values.audio_device_id = sdl2_config->GetString("Audio", "output_device", "auto"); + Settings::values.volume = static_cast<float>(sdl2_config->GetReal("Audio", "volume", 1)); + Settings::values.mic_input_device = + sdl2_config->GetString("Audio", "mic_input_device", "Default"); + Settings::values.mic_input_type = + static_cast<Settings::MicInputType>(sdl2_config->GetInteger("Audio", "mic_input_type", 1)); + + // Data Storage + Settings::values.use_virtual_sd = + sdl2_config->GetBoolean("Data Storage", "use_virtual_sd", true); + + // System + Settings::values.is_new_3ds = sdl2_config->GetBoolean("System", "is_new_3ds", true); + Settings::values.region_value = + sdl2_config->GetInteger("System", "region_value", Settings::REGION_VALUE_AUTO_SELECT); + Settings::values.init_clock = + static_cast<Settings::InitClock>(sdl2_config->GetInteger("System", "init_clock", 0)); + { + std::tm t; + t.tm_sec = 1; + t.tm_min = 0; + t.tm_hour = 0; + t.tm_mday = 1; + t.tm_mon = 0; + t.tm_year = 100; + t.tm_isdst = 0; + std::istringstream string_stream( + sdl2_config->GetString("System", "init_time", "2000-01-01 00:00:01")); + string_stream >> std::get_time(&t, "%Y-%m-%d %H:%M:%S"); + if (string_stream.fail()) { + LOG_ERROR(Config, "Failed To parse init_time. Using 2000-01-01 00:00:01"); + } + Settings::values.init_time = + std::chrono::duration_cast<std::chrono::seconds>( + std::chrono::system_clock::from_time_t(std::mktime(&t)).time_since_epoch()) + .count(); + } + + // Camera + using namespace Service::CAM; + Settings::values.camera_name[OuterRightCamera] = + sdl2_config->GetString("Camera", "camera_outer_right_name", "ndk"); + Settings::values.camera_config[OuterRightCamera] = sdl2_config->GetString( + "Camera", "camera_outer_right_config", std::string{Camera::NDK::BackCameraPlaceholder}); + Settings::values.camera_flip[OuterRightCamera] = + sdl2_config->GetInteger("Camera", "camera_outer_right_flip", 0); + Settings::values.camera_name[InnerCamera] = + sdl2_config->GetString("Camera", "camera_inner_name", "ndk"); + Settings::values.camera_config[InnerCamera] = sdl2_config->GetString( + "Camera", "camera_inner_config", std::string{Camera::NDK::FrontCameraPlaceholder}); + Settings::values.camera_flip[InnerCamera] = + sdl2_config->GetInteger("Camera", "camera_inner_flip", 0); + Settings::values.camera_name[OuterLeftCamera] = + sdl2_config->GetString("Camera", "camera_outer_left_name", "ndk"); + Settings::values.camera_config[OuterLeftCamera] = sdl2_config->GetString( + "Camera", "camera_outer_left_config", std::string{Camera::NDK::BackCameraPlaceholder}); + Settings::values.camera_flip[OuterLeftCamera] = + sdl2_config->GetInteger("Camera", "camera_outer_left_flip", 0); + + // Miscellaneous + Settings::values.log_filter = sdl2_config->GetString("Miscellaneous", "log_filter", "*:Info"); + + // Debugging + Settings::values.record_frame_times = + sdl2_config->GetBoolean("Debugging", "record_frame_times", false); + Settings::values.use_gdbstub = sdl2_config->GetBoolean("Debugging", "use_gdbstub", false); + Settings::values.gdbstub_port = + static_cast<u16>(sdl2_config->GetInteger("Debugging", "gdbstub_port", 24689)); + + for (const auto& service_module : Service::service_module_map) { + bool use_lle = sdl2_config->GetBoolean("Debugging", "LLE\\" + service_module.name, false); + Settings::values.lle_modules.emplace(service_module.name, use_lle); + } + + // Web Service + Settings::values.enable_telemetry = + sdl2_config->GetBoolean("WebService", "enable_telemetry", true); + Settings::values.web_api_url = + sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org"); + Settings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", ""); + Settings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", ""); + + // Update CFG file based on settings + UpdateCFG(); +} + +void Config::Reload() { + LoadINI(DefaultINI::sdl2_config_file); + ReadValues(); +} diff --git a/src/android/app/src/main/jni/config.h b/src/android/app/src/main/jni/config.h new file mode 100644 index 000000000..465457856 --- /dev/null +++ b/src/android/app/src/main/jni/config.h @@ -0,0 +1,26 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <string> + +class INIReader; + +class Config { +private: + std::unique_ptr<INIReader> sdl2_config; + std::string sdl2_config_loc; + + bool LoadINI(const std::string& default_contents = "", bool retry = true); + void ReadValues(); + void UpdateCFG(); + +public: + Config(); + ~Config(); + + void Reload(); +}; diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h new file mode 100644 index 000000000..56ab75e1a --- /dev/null +++ b/src/android/app/src/main/jni/default_ini.h @@ -0,0 +1,323 @@ +// Copyright 2014 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +namespace DefaultINI { + +const char* sdl2_config_file = R"( +[Controls] +# The input devices and parameters for each 3DS native input +# It should be in the format of "engine:[engine_name],[param1]:[value1],[param2]:[value2]..." +# Escape characters $0 (for ':'), $1 (for ',') and $2 (for '$') can be used in values + +# for button input, the following devices are available: +# - "keyboard" (default) for keyboard input. Required parameters: +# - "code": the code of the key to bind +# - "sdl" for joystick input using SDL. Required parameters: +# - "joystick": the index of the joystick to bind +# - "button"(optional): the index of the button to bind +# - "hat"(optional): the index of the hat to bind as direction buttons +# - "axis"(optional): the index of the axis to bind +# - "direction"(only used for hat): the direction name of the hat to bind. Can be "up", "down", "left" or "right" +# - "threshold"(only used for axis): a float value in (-1.0, 1.0) which the button is +# triggered if the axis value crosses +# - "direction"(only used for axis): "+" means the button is triggered when the axis value +# is greater than the threshold; "-" means the button is triggered when the axis value +# is smaller than the threshold +button_a= +button_b= +button_x= +button_y= +button_up= +button_down= +button_left= +button_right= +button_l= +button_r= +button_start= +button_select= +button_debug= +button_gpio14= +button_zl= +button_zr= +button_home= + +# for analog input, the following devices are available: +# - "analog_from_button" (default) for emulating analog input from direction buttons. Required parameters: +# - "up", "down", "left", "right": sub-devices for each direction. +# Should be in the format as a button input devices using escape characters, for example, "engine$0keyboard$1code$00" +# - "modifier": sub-devices as a modifier. +# - "modifier_scale": a float number representing the applied modifier scale to the analog input. +# Must be in range of 0.0-1.0. Defaults to 0.5 +# - "sdl" for joystick input using SDL. Required parameters: +# - "joystick": the index of the joystick to bind +# - "axis_x": the index of the axis to bind as x-axis (default to 0) +# - "axis_y": the index of the axis to bind as y-axis (default to 1) +circle_pad= +c_stick= + +# for motion input, the following devices are available: +# - "motion_emu" (default) for emulating motion input from mouse input. Required parameters: +# - "update_period": update period in milliseconds (default to 100) +# - "sensitivity": the coefficient converting mouse movement to tilting angle (default to 0.01) +# - "tilt_clamp": the max value of the tilt angle in degrees (default to 90) +# - "cemuhookudp" reads motion input from a udp server that uses cemuhook's udp protocol +motion_device= + +# for touch input, the following devices are available: +# - "emu_window" (default) for emulating touch input from mouse input to the emulation window. No parameters required +# - "cemuhookudp" reads touch input from a udp server that uses cemuhook's udp protocol +# - "min_x", "min_y", "max_x", "max_y": defines the udp device's touch screen coordinate system +touch_device= engine:emu_window + +# Most desktop operating systems do not expose a way to poll the motion state of the controllers +# so as a way around it, cemuhook created a udp client/server protocol to broadcast the data directly +# from a controller device to the client program. Citra has a client that can connect and read +# from any cemuhook compatible motion program. + +# IPv4 address of the udp input server (Default "127.0.0.1") +udp_input_address= + +# Port of the udp input server. (Default 26760) +udp_input_port= + +# The pad to request data on. Should be between 0 (Pad 1) and 3 (Pad 4). (Default 0) +udp_pad_index= + +[Core] +# Whether to use the Just-In-Time (JIT) compiler for CPU emulation +# 0: Interpreter (slow), 1 (default): JIT (fast) +use_cpu_jit = + +# Change the Clock Frequency of the emulated 3DS CPU. +# Underclocking can increase the performance of the game at the risk of freezing. +# Overclocking may fix lag that happens on console, but also comes with the risk of freezing. +# Range is any positive integer (but we suspect 25 - 400 is a good idea) Default is 100 +cpu_clock_percentage = + +[Renderer] +# Whether to render using GLES or OpenGL +# 0: OpenGL, 1 (default): GLES +use_gles = + +# Whether to use software or hardware rendering. +# 0: Software, 1 (default): Hardware +use_hw_renderer = + +# Whether to use hardware shaders to emulate 3DS shaders +# 0: Software, 1 (default): Hardware +use_hw_shader = + +# Whether to use separable shaders to emulate 3DS shaders (macOS only) +# 0: Off (Default), 1 : On +separable_shader = + +# Whether to use accurate multiplication in hardware shaders +# 0: Off (Default. Faster, but causes issues in some games) 1: On (Slower, but correct) +shaders_accurate_mul = + +# Enable asynchronous GPU emulation +# 0: Off (Slower, but more accurate) 1: On (Default. Faster, but may cause issues in some games) +use_asynchronous_gpu_emulation = + +# Whether to use the Just-In-Time (JIT) compiler for shader emulation +# 0: Interpreter (slow), 1 (default): JIT (fast) +use_shader_jit = + +# Forces VSync on the display thread. Usually doesn't impact performance, but on some drivers it can +# so only turn this off if you notice a speed difference. +# 0: Off, 1 (default): On +use_vsync_new = + +# Reduce stuttering by storing and loading generated shaders to disk +# 0: Off, 1 (default. On) +use_disk_shader_cache = + +# Resolution scale factor +# 0: Auto (scales resolution to window size), 1: Native 3DS screen resolution, Otherwise a scale +# factor for the 3DS resolution +resolution_factor = + +# Whether to enable V-Sync (caps the framerate at 60FPS) or not. +# 0 (default): Off, 1: On +vsync_enabled = + +# Turns on the frame limiter, which will limit frames output to the target game speed +# 0: Off, 1: On (default) +use_frame_limit = + +# Limits the speed of the game to run no faster than this value as a percentage of target speed +# 1 - 9999: Speed limit as a percentage of target game speed. 100 (default) +frame_limit = + +# The clear color for the renderer. What shows up on the sides of the bottom screen. +# Must be in range of 0.0-1.0. Defaults to 0.0 for all. +bg_red = +bg_blue = +bg_green = + +# Whether and how Stereoscopic 3D should be rendered +# 0 (default): Off, 1: Side by Side, 2: Anaglyph, 3: Interlaced, 4: Reverse Interlaced, 5: Cardboard VR +render_3d = + +# Change 3D Intensity +# 0 - 100: Intensity. 0 (default) +factor_3d = + +# The name of the post processing shader to apply. +# Loaded from shaders if render_3d is off or side by side. +# Loaded from shaders/anaglyph if render_3d is anaglyph +pp_shader_name = + +# Whether to enable linear filtering or not +# This is required for some shaders to work correctly +# 0: Nearest, 1 (default): Linear +filter_mode = + +[Layout] +# Layout for the screen inside the render window. +# 0 (default): Default Top Bottom Screen, 1: Single Screen Only, 2: Large Screen Small Screen, 3: Side by Side +layout_option = + +# Toggle custom layout (using the settings below) on or off. +# 0 (default): Off, 1: On +custom_layout = + +# Screen placement when using Custom layout option +# 0x, 0y is the top left corner of the render window. +custom_top_left = +custom_top_top = +custom_top_right = +custom_top_bottom = +custom_bottom_left = +custom_bottom_top = +custom_bottom_right = +custom_bottom_bottom = + +# Swaps the prominent screen with the other screen. +# For example, if Single Screen is chosen, setting this to 1 will display the bottom screen instead of the top screen. +# 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent +swap_screen = + +# Screen placement settings when using Cardboard VR (render3d = 4) +# 30 - 100: Screen size as a percentage of the viewport. 85 (default) +cardboard_screen_size = +# -100 - 100: Screen X-Coordinate shift as a percentage of empty space. 0 (default) +cardboard_x_shift = +# -100 - 100: Screen Y-Coordinate shift as a percentage of empty space. 0 (default) +cardboard_y_shift = + +[Audio] +# Whether or not to enable DSP LLE +# 0 (default): No, 1: Yes +enable_dsp_lle = + +# Whether or not to run DSP LLE on a different thread +# 0 (default): No, 1: Yes +enable_dsp_lle_thread = + +# Which audio output engine to use. +# auto (default): Auto-select, null: No audio output, sdl2: SDL2 (if available) +output_engine = + +# Whether or not to enable the audio-stretching post-processing effect. +# This effect adjusts audio speed to match emulation speed and helps prevent audio stutter, +# at the cost of increasing audio latency. +# 0: No, 1 (default): Yes +enable_audio_stretching = + +# Which audio device to use. +# auto (default): Auto-select +output_device = + +# Which mic input type to use. +# 0: None, 1 (default): Real device, 2: Static noise +mic_input_type = + +# Output volume. +# 1.0 (default): 100%, 0.0; mute +volume = + +[Data Storage] +# Whether to create a virtual SD card. +# 1 (default): Yes, 0: No +use_virtual_sd = + +[System] +# The system model that Citra will try to emulate +# 0: Old 3DS (default), 1: New 3DS +is_new_3ds = + +# The system region that Citra will use during emulation +# -1: Auto-select (default), 0: Japan, 1: USA, 2: Europe, 3: Australia, 4: China, 5: Korea, 6: Taiwan +region_value = + +# The system language that Citra will use during emulation +# 0: Japanese, 1: English (default), 2: French, 3: German, 4: Italian, 5: Spanish, +# 6: Simplified Chinese, 7: Korean, 8: Dutch, 9: Portuguese, 10: Russian, 11: Traditional Chinese +language = + +# The clock to use when citra starts +# 0: System clock (default), 1: fixed time +init_clock = + +# Time used when init_clock is set to fixed_time in the format %Y-%m-%d %H:%M:%S +# set to fixed time. Default 2000-01-01 00:00:01 +# Note: 3DS can only handle times later then Jan 1 2000 +init_time = + +[Camera] +# Which camera engine to use for the right outer camera +# blank: a dummy camera that always returns black image +# image: loads a still image from the storage. When the camera is started, you will be prompted +# to select an image. +# ndk (Default): uses the device camera. You can specify the camera ID to use in the config field. +# If you don't specify an ID, the default setting will be used. For outer cameras, +# the back-facing camera will be used. For the inner camera, the front-facing +# camera will be used. Please note that 'Legacy' cameras are not supported. +camera_outer_right_name = + +# A config string for the right outer camera. Its meaning is defined by the camera engine +camera_outer_right_config = + +# The image flip to apply +# 0: None (default), 1: Horizontal, 2: Vertical, 3: Reverse +camera_outer_right_flip = + +# ... for the left outer camera +camera_outer_left_name = +camera_outer_left_config = +camera_outer_left_flip = + +# ... for the inner camera +camera_inner_name = +camera_inner_config = +camera_inner_flip = + +[Miscellaneous] +# A filter which removes logs below a certain logging level. +# Examples: *:Debug Kernel.SVC:Trace Service.*:Critical +log_filter = *:Info + +[Debugging] +# Record frame time data, can be found in the log directory. Boolean value +record_frame_times = +# Port for listening to GDB connections. +use_gdbstub=false +gdbstub_port=24689 +# To LLE a service module add "LLE\<module name>=true" + +[WebService] +# Whether or not to enable telemetry +# 0: No, 1 (default): Yes +enable_telemetry = +# URL for Web API +web_api_url = https://api.citra-emu.org +# Username and token for Citra Web Service +# See https://profile.citra-emu.org/ for more info +citra_username = +citra_token = +)"; +} diff --git a/src/android/app/src/main/jni/emu_window/emu_window.cpp b/src/android/app/src/main/jni/emu_window/emu_window.cpp new file mode 100644 index 000000000..86faef554 --- /dev/null +++ b/src/android/app/src/main/jni/emu_window/emu_window.cpp @@ -0,0 +1,275 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <algorithm> +#include <array> +#include <cstdlib> +#include <string> + +#include <android/native_window_jni.h> +#include <glad/glad.h> + +#include "common/logging/log.h" +#include "core/settings.h" +#include "input_common/main.h" +#include "jni/emu_window/emu_window.h" +#include "jni/id_cache.h" +#include "jni/input_manager.h" +#include "network/network.h" +#include "video_core/renderer_base.h" +#include "video_core/video_core.h" + +static constexpr std::array<EGLint, 15> egl_attribs{EGL_SURFACE_TYPE, + EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES3_BIT_KHR, + EGL_BLUE_SIZE, + 8, + EGL_GREEN_SIZE, + 8, + EGL_RED_SIZE, + 8, + EGL_DEPTH_SIZE, + 0, + EGL_STENCIL_SIZE, + 0, + EGL_NONE}; +static constexpr std::array<EGLint, 5> egl_empty_attribs{EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; +static constexpr std::array<EGLint, 4> egl_context_attribs{EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; + +SharedContext_Android::SharedContext_Android(EGLDisplay egl_display, EGLConfig egl_config, + EGLContext egl_share_context) + : egl_display{egl_display}, egl_surface{eglCreatePbufferSurface(egl_display, egl_config, + egl_empty_attribs.data())}, + egl_context{eglCreateContext(egl_display, egl_config, egl_share_context, + egl_context_attribs.data())} { + ASSERT_MSG(egl_surface, "eglCreatePbufferSurface() failed!"); + ASSERT_MSG(egl_context, "eglCreateContext() failed!"); +} + +SharedContext_Android::~SharedContext_Android() { + if (!eglDestroySurface(egl_display, egl_surface)) { + LOG_CRITICAL(Frontend, "eglDestroySurface() failed"); + } + + if (!eglDestroyContext(egl_display, egl_context)) { + LOG_CRITICAL(Frontend, "eglDestroySurface() failed"); + } +} + +void SharedContext_Android::MakeCurrent() { + eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context); +} + +void SharedContext_Android::DoneCurrent() { + eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); +} + +static bool IsPortraitMode() { + return JNI_FALSE != IDCache::GetEnvForThread()->CallStaticBooleanMethod( + IDCache::GetNativeLibraryClass(), IDCache::GetIsPortraitMode()); +} + +static void UpdateLandscapeScreenLayout() { + Settings::values.layout_option = + static_cast<Settings::LayoutOption>(IDCache::GetEnvForThread()->CallStaticIntMethod( + IDCache::GetNativeLibraryClass(), IDCache::GetLandscapeScreenLayout())); +} + +void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { + render_window = surface; + StopPresenting(); +} + +bool EmuWindow_Android::OnTouchEvent(int x, int y, bool pressed) { + if (pressed) { + return TouchPressed((unsigned)std::max(x, 0), (unsigned)std::max(y, 0)); + } + + TouchReleased(); + return true; +} + +void EmuWindow_Android::OnTouchMoved(int x, int y) { + TouchMoved((unsigned)std::max(x, 0), (unsigned)std::max(y, 0)); +} + +void EmuWindow_Android::OnFramebufferSizeChanged() { + UpdateLandscapeScreenLayout(); + const bool is_portrait_mode{IsPortraitMode()}; + const int bigger{window_width > window_height ? window_width : window_height}; + const int smaller{window_width < window_height ? window_width : window_height}; + if (is_portrait_mode) { + UpdateCurrentFramebufferLayout(smaller, bigger, is_portrait_mode); + } else { + UpdateCurrentFramebufferLayout(bigger, smaller, is_portrait_mode); + } +} + +EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface) { + LOG_DEBUG(Frontend, "Initializing EmuWindow_Android"); + + if (!surface) { + LOG_CRITICAL(Frontend, "surface is nullptr"); + return; + } + + Network::Init(); + + host_window = surface; + + if (egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); egl_display == EGL_NO_DISPLAY) { + LOG_CRITICAL(Frontend, "eglGetDisplay() failed"); + return; + } + if (eglInitialize(egl_display, 0, 0) != EGL_TRUE) { + LOG_CRITICAL(Frontend, "eglInitialize() failed"); + return; + } + if (EGLint egl_num_configs{}; eglChooseConfig(egl_display, egl_attribs.data(), &egl_config, 1, + &egl_num_configs) != EGL_TRUE) { + LOG_CRITICAL(Frontend, "eglChooseConfig() failed"); + return; + } + + CreateWindowSurface(); + + if (eglQuerySurface(egl_display, egl_surface, EGL_WIDTH, &window_width) != EGL_TRUE) { + return; + } + if (eglQuerySurface(egl_display, egl_surface, EGL_HEIGHT, &window_height) != EGL_TRUE) { + return; + } + + if (egl_context = eglCreateContext(egl_display, egl_config, 0, egl_context_attribs.data()); + egl_context == EGL_NO_CONTEXT) { + LOG_CRITICAL(Frontend, "eglCreateContext() failed"); + return; + } + if (eglSurfaceAttrib(egl_display, egl_surface, EGL_SWAP_BEHAVIOR, EGL_BUFFER_DESTROYED) != + EGL_TRUE) { + LOG_CRITICAL(Frontend, "eglSurfaceAttrib() failed"); + return; + } + if (core_context = CreateSharedContext(); !core_context) { + LOG_CRITICAL(Frontend, "CreateSharedContext() failed"); + return; + } + if (eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context) != EGL_TRUE) { + LOG_CRITICAL(Frontend, "eglMakeCurrent() failed"); + return; + } + if (!gladLoadGLES2Loader((GLADloadproc)eglGetProcAddress)) { + LOG_CRITICAL(Frontend, "gladLoadGLES2Loader() failed"); + return; + } + if (!eglSwapInterval(egl_display, Settings::values.use_vsync_new ? 1 : 0)) { + LOG_CRITICAL(Frontend, "eglSwapInterval() failed"); + return; + } + + OnFramebufferSizeChanged(); +} + +bool EmuWindow_Android::CreateWindowSurface() { + if (!host_window) { + return true; + } + + EGLint format{}; + eglGetConfigAttrib(egl_display, egl_config, EGL_NATIVE_VISUAL_ID, &format); + ANativeWindow_setBuffersGeometry(host_window, 0, 0, format); + + if (egl_surface = eglCreateWindowSurface(egl_display, egl_config, host_window, 0); + egl_surface == EGL_NO_SURFACE) { + return {}; + } + + return !!egl_surface; +} + +void EmuWindow_Android::DestroyWindowSurface() { + if (!egl_surface) { + return; + } + if (eglGetCurrentSurface(EGL_DRAW) == egl_surface) { + eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + if (!eglDestroySurface(egl_display, egl_surface)) { + LOG_CRITICAL(Frontend, "eglDestroySurface() failed"); + } + egl_surface = EGL_NO_SURFACE; +} + +void EmuWindow_Android::DestroyContext() { + if (!egl_context) { + return; + } + if (eglGetCurrentContext() == egl_context) { + eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + if (!eglDestroyContext(egl_display, egl_context)) { + LOG_CRITICAL(Frontend, "eglDestroySurface() failed"); + } + if (!eglTerminate(egl_display)) { + LOG_CRITICAL(Frontend, "eglTerminate() failed"); + } + egl_context = EGL_NO_CONTEXT; + egl_display = EGL_NO_DISPLAY; +} + +EmuWindow_Android::~EmuWindow_Android() { + DestroyWindowSurface(); + DestroyContext(); +} + +std::unique_ptr<Frontend::GraphicsContext> EmuWindow_Android::CreateSharedContext() const { + return std::make_unique<SharedContext_Android>(egl_display, egl_config, egl_context); +} + +void EmuWindow_Android::StopPresenting() { + if (presenting_state == PresentingState::Running) { + eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + presenting_state = PresentingState::Stopped; +} + +void EmuWindow_Android::TryPresenting() { + if (presenting_state != PresentingState::Running) { + if (presenting_state == PresentingState::Initial) { + eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + presenting_state = PresentingState::Running; + } else { + return; + } + } + eglSwapInterval(egl_display, Settings::values.use_vsync_new ? 1 : 0); + if (VideoCore::g_renderer) { + VideoCore::g_renderer->TryPresent(0); + eglSwapBuffers(egl_display, egl_surface); + } +} + +void EmuWindow_Android::PollEvents() { + if (!render_window) { + return; + } + + host_window = render_window; + render_window = nullptr; + + DestroyWindowSurface(); + CreateWindowSurface(); + OnFramebufferSizeChanged(); + presenting_state = PresentingState::Initial; +} + +void EmuWindow_Android::MakeCurrent() { + core_context->MakeCurrent(); +} + +void EmuWindow_Android::DoneCurrent() { + core_context->DoneCurrent(); +} diff --git a/src/android/app/src/main/jni/emu_window/emu_window.h b/src/android/app/src/main/jni/emu_window/emu_window.h new file mode 100644 index 000000000..10a293c96 --- /dev/null +++ b/src/android/app/src/main/jni/emu_window/emu_window.h @@ -0,0 +1,83 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <vector> + +#include <EGL/egl.h> +#include <EGL/eglext.h> + +#include "core/frontend/emu_window.h" + +struct ANativeWindow; + +class SharedContext_Android : public Frontend::GraphicsContext { +public: + SharedContext_Android(EGLDisplay egl_display, EGLConfig egl_config, + EGLContext egl_share_context); + + ~SharedContext_Android() override; + + void MakeCurrent() override; + + void DoneCurrent() override; + +private: + EGLDisplay egl_display{}; + EGLSurface egl_surface{}; + EGLContext egl_context{}; +}; + +class EmuWindow_Android : public Frontend::EmuWindow { +public: + EmuWindow_Android(ANativeWindow* surface); + ~EmuWindow_Android(); + + void Present(); + + /// Called by the onSurfaceChanges() method to change the surface + void OnSurfaceChanged(ANativeWindow* surface); + + /// Handles touch event that occur.(Touched or released) + bool OnTouchEvent(int x, int y, bool pressed); + + /// Handles movement of touch pointer + void OnTouchMoved(int x, int y); + + void PollEvents() override; + void MakeCurrent() override; + void DoneCurrent() override; + + void TryPresenting(); + void StopPresenting(); + + std::unique_ptr<GraphicsContext> CreateSharedContext() const override; + +private: + void OnFramebufferSizeChanged(); + bool CreateWindowSurface(); + void DestroyWindowSurface(); + void DestroyContext(); + + ANativeWindow* render_window{}; + ANativeWindow* host_window{}; + + int window_width{}; + int window_height{}; + + EGLConfig egl_config; + EGLSurface egl_surface{}; + EGLContext egl_context{}; + EGLDisplay egl_display{}; + + std::unique_ptr<Frontend::GraphicsContext> core_context; + + enum class PresentingState { + Initial, + Running, + Stopped, + }; + PresentingState presenting_state{}; +}; diff --git a/src/android/app/src/main/jni/game_info.cpp b/src/android/app/src/main/jni/game_info.cpp new file mode 100644 index 000000000..80c0379d6 --- /dev/null +++ b/src/android/app/src/main/jni/game_info.cpp @@ -0,0 +1,150 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <cstring> +#include <map> +#include <memory> +#include <vector> + +#include "common/string_util.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/fs/archive.h" +#include "core/loader/loader.h" +#include "core/loader/smdh.h" +#include "jni/game_info.h" + +namespace GameInfo { + +std::vector<u8> GetSMDHData(std::string physical_name) { + std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(physical_name); + if (!loader) { + return {}; + } + + u64 program_id = 0; + loader->ReadProgramId(program_id); + + std::vector<u8> smdh = [program_id, &loader]() -> std::vector<u8> { + std::vector<u8> original_smdh; + loader->ReadIcon(original_smdh); + + if (program_id < 0x00040000'00000000 || program_id > 0x00040000'FFFFFFFF) + return original_smdh; + + std::string update_path = Service::AM::GetTitleContentPath( + Service::FS::MediaType::SDMC, program_id + 0x0000000E'00000000); + + if (!FileUtil::Exists(update_path)) + return original_smdh; + + std::unique_ptr<Loader::AppLoader> update_loader = Loader::GetLoader(update_path); + + if (!update_loader) + return original_smdh; + + std::vector<u8> update_smdh; + update_loader->ReadIcon(update_smdh); + return update_smdh; + }(); + + return smdh; +} + +std::u16string GetTitle(std::string physical_name) { + Loader::SMDH::TitleLanguage language = Loader::SMDH::TitleLanguage::English; + std::vector<u8> smdh_data = GetSMDHData(physical_name); + + if (!Loader::IsValidSMDH(smdh_data)) { + // SMDH is not valid, return null + return {}; + } + + Loader::SMDH smdh; + memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); + + // Get the title from SMDH in UTF-16 format + std::u16string title{ + reinterpret_cast<char16_t*>(smdh.titles[static_cast<int>(language)].long_title.data())}; + + return title; +} + +std::u16string GetPublisher(std::string physical_name) { + Loader::SMDH::TitleLanguage language = Loader::SMDH::TitleLanguage::English; + std::vector<u8> smdh_data = GetSMDHData(physical_name); + + if (!Loader::IsValidSMDH(smdh_data)) { + // SMDH is not valid, return null + return {}; + } + + Loader::SMDH smdh; + memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); + + // Get the Publisher's name from SMDH in UTF-16 format + char16_t* publisher; + publisher = + reinterpret_cast<char16_t*>(smdh.titles[static_cast<int>(language)].publisher.data()); + + return publisher; +} + +std::string GetRegions(std::string physical_name) { + std::vector<u8> smdh_data = GetSMDHData(physical_name); + + if (!Loader::IsValidSMDH(smdh_data)) { + // SMDH is not valid, return "Invalid region" + return "Invalid region"; + } + + Loader::SMDH smdh; + memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); + + using GameRegion = Loader::SMDH::GameRegion; + static const std::map<GameRegion, const char*> regions_map = { + {GameRegion::Japan, "Japan"}, {GameRegion::NorthAmerica, "North America"}, + {GameRegion::Europe, "Europe"}, {GameRegion::Australia, "Australia"}, + {GameRegion::China, "China"}, {GameRegion::Korea, "Korea"}, + {GameRegion::Taiwan, "Taiwan"}}; + std::vector<GameRegion> regions = smdh.GetRegions(); + + if (regions.empty()) { + return "Invalid region"; + } + + const bool region_free = + std::all_of(regions_map.begin(), regions_map.end(), [®ions](const auto& it) { + return std::find(regions.begin(), regions.end(), it.first) != regions.end(); + }); + + if (region_free) { + return "Region free"; + } + + const std::string separator = ", "; + std::string result = regions_map.at(regions.front()); + for (auto region = ++regions.begin(); region != regions.end(); ++region) { + result += separator + regions_map.at(*region); + } + + return result; +} + +std::vector<u16> GetIcon(std::string physical_name) { + std::vector<u8> smdh_data = GetSMDHData(physical_name); + + if (!Loader::IsValidSMDH(smdh_data)) { + // SMDH is not valid, return null + return std::vector<u16>(0, 0); + } + + Loader::SMDH smdh; + memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); + + // Always get a 48x48(large) icon + std::vector<u16> icon_data = smdh.GetIcon(true); + return icon_data; +} + +} // namespace GameInfo diff --git a/src/android/app/src/main/jni/game_info.h b/src/android/app/src/main/jni/game_info.h new file mode 100644 index 000000000..ad3254aab --- /dev/null +++ b/src/android/app/src/main/jni/game_info.h @@ -0,0 +1,20 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <cstring> +#include <string> + +#include "common/common_types.h" + +namespace GameInfo { +std::vector<u8> GetSMDHData(std::string physical_name); + +std::u16string GetTitle(std::string physical_name); + +std::u16string GetPublisher(std::string physical_name); + +std::string GetRegions(std::string physical_name); + +std::vector<u16> GetIcon(std::string physical_name); +} // namespace GameInfo diff --git a/src/android/app/src/main/jni/game_settings.cpp b/src/android/app/src/main/jni/game_settings.cpp new file mode 100644 index 000000000..06cefc91f --- /dev/null +++ b/src/android/app/src/main/jni/game_settings.cpp @@ -0,0 +1,81 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "core/settings.h" + +namespace GameSettings { + +void LoadOverrides(u64 program_id) { + Settings::values.gpu_timing_mode_submit_list = Settings::GpuTimingMode::Asynch_1ms; + Settings::values.gpu_timing_mode_swap_buffers = Settings::GpuTimingMode::Asynch_8ms; + Settings::values.gpu_timing_mode_memory_fill = Settings::GpuTimingMode::Asynch_2ms; + Settings::values.gpu_timing_mode_display_transfer = Settings::GpuTimingMode::Synch; + Settings::values.gpu_timing_mode_flush = Settings::GpuTimingMode::Skip; + Settings::values.gpu_timing_mode_flush_and_invalidate = Settings::GpuTimingMode::Asynch; + Settings::values.gpu_timing_mode_invalidate = Settings::GpuTimingMode::Synch; + + switch (program_id) { + // JAP / Dragon Quest VII: Fragments of the Forgotten Past + case 0x0004000000065E00: + // USA / Dragon Quest VII: Fragments of the Forgotten Past + case 0x000400000018EF00: + // EUR / Dragon Quest VII: Fragments of the Forgotten Past + case 0x000400000018F000: + // This game is currently broken with asynchronous GPU + Settings::values.use_asynchronous_gpu_emulation = false; + break; + + // JAP / The Legend of Zelda: Ocarina of Time 3D + case 0x0004000000033400: + // USA / The Legend of Zelda: Ocarina of Time 3D + case 0x0004000000033500: + // EUR / The Legend of Zelda: Ocarina of Time 3D + case 0x0004000000033600: + // KOR / The Legend of Zelda: Ocarina of Time 3D + case 0x000400000008F800: + // CHI / The Legend of Zelda: Ocarina of Time 3D + case 0x000400000008F900: + // This game requires accurate multiplication to render properly + Settings::values.shaders_accurate_mul = true; + Settings::values.gpu_timing_mode_submit_list = Settings::GpuTimingMode::Asynch_1ms; + Settings::values.gpu_timing_mode_swap_buffers = Settings::GpuTimingMode::Asynch_4ms; + Settings::values.gpu_timing_mode_memory_fill = Settings::GpuTimingMode::Asynch; + Settings::values.gpu_timing_mode_display_transfer = Settings::GpuTimingMode::Asynch; + Settings::values.gpu_timing_mode_flush = Settings::GpuTimingMode::Skip; + Settings::values.gpu_timing_mode_flush_and_invalidate = Settings::GpuTimingMode::Skip; + break; + + // JAP / Super Mario 3D Land + case 0x0004000000054100: + // USA / Super Mario 3D Land + case 0x0004000000054000: + // EUR / Super Mario 3D Land + case 0x0004000000053F00: + // KOR / Super Mario 3D Land + case 0x0004000000089D00: + // This game has very sensitive timings with asynchronous GPU + Settings::values.gpu_timing_mode_submit_list = Settings::GpuTimingMode::Synch; + break; + + // USA / Mario & Luigi: Superstar Saga + Bowsers Minions + case 0x00040000001B8F00: + // EUR / Mario & Luigi: Superstar Saga + Bowsers Minions + case 0x00040000001B9000: + // JAP / Mario & Luigi: Superstar Saga + Bowsers Minions + case 0x0004000000194B00: + // This game requires accurate multiplication to render properly + Settings::values.shaders_accurate_mul = true; + break; + + // USA / Mario & Luigi: Bowsers Inside Story + Bowser Jrs Journey + case 0x00040000001D1400: + // EUR / Mario & Luigi: Bowsers Inside Story + Bowser Jrs Journey + case 0x00040000001D1500: + // This game requires accurate multiplication to render properly + Settings::values.shaders_accurate_mul = true; + break; + } +} + +} // namespace GameSettings diff --git a/src/android/app/src/main/jni/game_settings.h b/src/android/app/src/main/jni/game_settings.h new file mode 100644 index 000000000..b034e1865 --- /dev/null +++ b/src/android/app/src/main/jni/game_settings.h @@ -0,0 +1,11 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/common_types.h" + +namespace GameSettings { + +void LoadOverrides(u64 program_id); + +} // namespace GameSettings diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp new file mode 100644 index 000000000..cf1c24437 --- /dev/null +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -0,0 +1,235 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/common_paths.h" +#include "common/logging/backend.h" +#include "common/logging/filter.h" +#include "common/logging/log.h" +#include "core/settings.h" +#include "jni/applets/mii_selector.h" +#include "jni/applets/swkbd.h" +#include "jni/camera/still_image_camera.h" +#include "jni/id_cache.h" + +#include <jni.h> + +static constexpr jint JNI_VERSION = JNI_VERSION_1_6; + +static JavaVM* s_java_vm; + +static jclass s_native_library_class; +static jclass s_core_error_class; +static jclass s_savestate_info_class; +static jclass s_disk_cache_progress_class; +static jclass s_load_callback_stage_class; +static jmethodID s_on_core_error; +static jmethodID s_display_alert_msg; +static jmethodID s_display_alert_prompt; +static jmethodID s_alert_prompt_button; +static jmethodID s_is_portrait_mode; +static jmethodID s_landscape_screen_layout; +static jmethodID s_exit_emulation_activity; +static jmethodID s_request_camera_permission; +static jmethodID s_request_mic_permission; +static jmethodID s_disk_cache_load_progress; + +static std::unordered_map<VideoCore::LoadCallbackStage, jobject> s_java_load_callback_stages; + +namespace IDCache { + +JNIEnv* GetEnvForThread() { + thread_local static struct OwnedEnv { + OwnedEnv() { + status = s_java_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6); + if (status == JNI_EDETACHED) + s_java_vm->AttachCurrentThread(&env, nullptr); + } + + ~OwnedEnv() { + if (status == JNI_EDETACHED) + s_java_vm->DetachCurrentThread(); + } + + int status; + JNIEnv* env = nullptr; + } owned; + return owned.env; +} + +jclass GetNativeLibraryClass() { + return s_native_library_class; +} + +jclass GetCoreErrorClass() { + return s_core_error_class; +} + +jclass GetSavestateInfoClass() { + return s_savestate_info_class; +} + +jclass GetDiskCacheProgressClass() { + return s_disk_cache_progress_class; +} + +jclass GetDiskCacheLoadCallbackStageClass() { + return s_load_callback_stage_class; +} + +jmethodID GetOnCoreError() { + return s_on_core_error; +} + +jmethodID GetDisplayAlertMsg() { + return s_display_alert_msg; +} + +jmethodID GetDisplayAlertPrompt() { + return s_display_alert_prompt; +} + +jmethodID GetAlertPromptButton() { + return s_alert_prompt_button; +} + +jmethodID GetIsPortraitMode() { + return s_is_portrait_mode; +} + +jmethodID GetLandscapeScreenLayout() { + return s_landscape_screen_layout; +} + +jmethodID GetExitEmulationActivity() { + return s_exit_emulation_activity; +} + +jmethodID GetRequestCameraPermission() { + return s_request_camera_permission; +} + +jmethodID GetRequestMicPermission() { + return s_request_mic_permission; +} + +jmethodID GetDiskCacheLoadProgress() { + return s_disk_cache_load_progress; +} + +jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage) { + const auto it = s_java_load_callback_stages.find(stage); + ASSERT_MSG(it != s_java_load_callback_stages.end(), "Invalid LoadCallbackStage: {}", stage); + + return it->second; +} + +} // namespace IDCache + +#ifdef __cplusplus +extern "C" { +#endif + +jint JNI_OnLoad(JavaVM* vm, void* reserved) { + s_java_vm = vm; + + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK) + return JNI_ERR; + + // Initialize Logger + Log::Filter log_filter; + log_filter.ParseFilterString(Settings::values.log_filter); + Log::SetGlobalFilter(log_filter); + Log::AddBackend(std::make_unique<Log::LogcatBackend>()); + FileUtil::CreateFullPath(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); + Log::AddBackend(std::make_unique<Log::FileBackend>( + FileUtil::GetUserPath(FileUtil::UserPath::LogDir) + LOG_FILE)); + LOG_INFO(Frontend, "Logging backend initialised"); + + // Initialize Java classes + const jclass native_library_class = env->FindClass("org/citra/citra_emu/NativeLibrary"); + s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class)); + s_savestate_info_class = reinterpret_cast<jclass>( + env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$SavestateInfo"))); + s_core_error_class = reinterpret_cast<jclass>( + env->NewGlobalRef(env->FindClass("org/citra/citra_emu/NativeLibrary$CoreError"))); + s_disk_cache_progress_class = reinterpret_cast<jclass>(env->NewGlobalRef( + env->FindClass("org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress"))); + s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass( + "org/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage"))); + + // Initialize Java methods + s_on_core_error = env->GetStaticMethodID( + s_native_library_class, "OnCoreError", + "(Lorg/citra/citra_emu/NativeLibrary$CoreError;Ljava/lang/String;)Z"); + s_display_alert_msg = env->GetStaticMethodID(s_native_library_class, "displayAlertMsg", + "(Ljava/lang/String;Ljava/lang/String;Z)Z"); + s_display_alert_prompt = + env->GetStaticMethodID(s_native_library_class, "displayAlertPrompt", + "(Ljava/lang/String;Ljava/lang/String;I)Ljava/lang/String;"); + s_alert_prompt_button = + env->GetStaticMethodID(s_native_library_class, "alertPromptButton", "()I"); + s_is_portrait_mode = env->GetStaticMethodID(s_native_library_class, "isPortraitMode", "()Z"); + s_landscape_screen_layout = + env->GetStaticMethodID(s_native_library_class, "landscapeScreenLayout", "()I"); + s_exit_emulation_activity = + env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V"); + s_request_camera_permission = + env->GetStaticMethodID(s_native_library_class, "RequestCameraPermission", "()Z"); + s_request_mic_permission = + env->GetStaticMethodID(s_native_library_class, "RequestMicPermission", "()Z"); + s_disk_cache_load_progress = env->GetStaticMethodID( + s_disk_cache_progress_class, "loadProgress", + "(Lorg/citra/citra_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage;II)V"); + + // Initialize LoadCallbackStage map + const auto to_java_load_callback_stage = [env](const std::string& stage) { + jclass load_callback_stage_class = IDCache::GetDiskCacheLoadCallbackStageClass(); + return env->NewGlobalRef(env->GetStaticObjectField( + load_callback_stage_class, + env->GetStaticFieldID(load_callback_stage_class, stage.c_str(), + "Lorg/citra/citra_emu/disk_shader_cache/" + "DiskShaderCacheProgress$LoadCallbackStage;"))); + }; + + s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Prepare, + to_java_load_callback_stage("Prepare")); + s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Decompile, + to_java_load_callback_stage("Decompile")); + s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Build, + to_java_load_callback_stage("Build")); + s_java_load_callback_stages.emplace(VideoCore::LoadCallbackStage::Complete, + to_java_load_callback_stage("Complete")); + + MiiSelector::InitJNI(env); + SoftwareKeyboard::InitJNI(env); + Camera::StillImage::InitJNI(env); + + return JNI_VERSION; +} + +void JNI_OnUnload(JavaVM* vm, void* reserved) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK) { + return; + } + + env->DeleteGlobalRef(s_native_library_class); + env->DeleteGlobalRef(s_savestate_info_class); + env->DeleteGlobalRef(s_core_error_class); + env->DeleteGlobalRef(s_disk_cache_progress_class); + env->DeleteGlobalRef(s_load_callback_stage_class); + + for (auto& [key, object] : s_java_load_callback_stages) { + env->DeleteGlobalRef(object); + } + + MiiSelector::CleanupJNI(env); + SoftwareKeyboard::CleanupJNI(env); + Camera::StillImage::CleanupJNI(env); +} + +#ifdef __cplusplus +} +#endif diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h new file mode 100644 index 000000000..4b8c89511 --- /dev/null +++ b/src/android/app/src/main/jni/id_cache.h @@ -0,0 +1,50 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <type_traits> +#include <jni.h> +#include "video_core/rasterizer_interface.h" + +namespace IDCache { + +JNIEnv* GetEnvForThread(); +jclass GetNativeLibraryClass(); +jclass GetCoreErrorClass(); +jclass GetSavestateInfoClass(); +jclass GetDiskCacheProgressClass(); +jclass GetDiskCacheLoadCallbackStageClass(); +jmethodID GetOnCoreError(); +jmethodID GetDisplayAlertMsg(); +jmethodID GetDisplayAlertPrompt(); +jmethodID GetAlertPromptButton(); +jmethodID GetIsPortraitMode(); +jmethodID GetLandscapeScreenLayout(); +jmethodID GetExitEmulationActivity(); +jmethodID GetRequestCameraPermission(); +jmethodID GetRequestMicPermission(); +jmethodID GetDiskCacheLoadProgress(); + +jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage); + +} // namespace IDCache + +template <typename T = jobject> +using SharedGlobalRef = std::shared_ptr<std::remove_pointer_t<T>>; + +struct SharedGlobalRefDeleter { + void operator()(jobject ptr) { + JNIEnv* env = IDCache::GetEnvForThread(); + env->DeleteGlobalRef(ptr); + } +}; + +template <typename T = jobject> +SharedGlobalRef<T> NewSharedGlobalRef(T object) { + JNIEnv* env = IDCache::GetEnvForThread(); + auto* global_ref = reinterpret_cast<T>(env->NewGlobalRef(object)); + return SharedGlobalRef<T>(global_ref, SharedGlobalRefDeleter()); +} diff --git a/src/android/app/src/main/jni/input_manager.cpp b/src/android/app/src/main/jni/input_manager.cpp new file mode 100644 index 000000000..9c58e3527 --- /dev/null +++ b/src/android/app/src/main/jni/input_manager.cpp @@ -0,0 +1,332 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <cmath> +#include <list> +#include <mutex> +#include <string> +#include <tuple> +#include <unordered_map> +#include <utility> +#include "common/logging/log.h" +#include "common/math_util.h" +#include "common/param_package.h" +#include "input_common/main.h" +#include "input_common/sdl/sdl.h" +#include "jni/input_manager.h" +#include "jni/ndk_motion.h" + +namespace InputManager { + +static std::shared_ptr<ButtonFactory> button; +static std::shared_ptr<AnalogFactory> analog; +static std::shared_ptr<NDKMotionFactory> motion; + +// Button Handler +class KeyButton final : public Input::ButtonDevice { +public: + explicit KeyButton(std::shared_ptr<ButtonList> button_list_) : button_list(button_list_) {} + + ~KeyButton(); + + bool GetStatus() const override { + return status.load(); + } + + friend class ButtonList; + +private: + std::shared_ptr<ButtonList> button_list; + std::atomic<bool> status{false}; +}; + +struct KeyButtonPair { + int button_id; + KeyButton* key_button; +}; + +class ButtonList { +public: + void AddButton(int button_id, KeyButton* key_button) { + std::lock_guard<std::mutex> guard(mutex); + list.push_back(KeyButtonPair{button_id, key_button}); + } + + void RemoveButton(const KeyButton* key_button) { + std::lock_guard<std::mutex> guard(mutex); + list.remove_if( + [key_button](const KeyButtonPair& pair) { return pair.key_button == key_button; }); + } + + bool ChangeButtonStatus(int button_id, bool pressed) { + std::lock_guard<std::mutex> guard(mutex); + bool button_found = false; + for (const KeyButtonPair& pair : list) { + if (pair.button_id == button_id) { + pair.key_button->status.store(pressed); + button_found = true; + } + } + // If we don't find the button don't consume the button press event + return button_found; + } + + void ChangeAllButtonStatus(bool pressed) { + std::lock_guard<std::mutex> guard(mutex); + for (const KeyButtonPair& pair : list) { + pair.key_button->status.store(pressed); + } + } + +private: + std::mutex mutex; + std::list<KeyButtonPair> list; +}; + +KeyButton::~KeyButton() { + button_list->RemoveButton(this); +} + +// Analog Button +class AnalogButton final : public Input::ButtonDevice { +public: + explicit AnalogButton(std::shared_ptr<AnalogButtonList> button_list_, float threshold_, + bool trigger_if_greater_) + : button_list(button_list_), threshold(threshold_), + trigger_if_greater(trigger_if_greater_) {} + + ~AnalogButton(); + + bool GetStatus() const override { + if (trigger_if_greater) + return axis_val.load() > threshold; + return axis_val.load() < threshold; + } + + friend class AnalogButtonList; + +private: + std::shared_ptr<AnalogButtonList> button_list; + std::atomic<float> axis_val{0.0f}; + float threshold; + bool trigger_if_greater; +}; + +struct AnalogButtonPair { + int axis_id; + AnalogButton* key_button; +}; + +class AnalogButtonList { +public: + void AddAnalogButton(int button_id, AnalogButton* key_button) { + std::lock_guard<std::mutex> guard(mutex); + list.push_back(AnalogButtonPair{button_id, key_button}); + } + + void RemoveButton(const AnalogButton* key_button) { + std::lock_guard<std::mutex> guard(mutex); + list.remove_if( + [key_button](const AnalogButtonPair& pair) { return pair.key_button == key_button; }); + } + + bool ChangeAxisValue(int axis_id, float axis) { + std::lock_guard<std::mutex> guard(mutex); + bool button_found = false; + for (const AnalogButtonPair& pair : list) { + if (pair.axis_id == axis_id) { + pair.key_button->axis_val.store(axis); + button_found = true; + } + } + // If we don't find the button don't consume the button press event + return button_found; + } + +private: + std::mutex mutex; + std::list<AnalogButtonPair> list; +}; + +AnalogButton::~AnalogButton() { + button_list->RemoveButton(this); +} + +// Joystick Handler +class Joystick final : public Input::AnalogDevice { +public: + explicit Joystick(std::shared_ptr<AnalogList> analog_list_) : analog_list(analog_list_) {} + + ~Joystick(); + + std::tuple<float, float> GetStatus() const override { + return std::make_tuple(x_axis.load(), y_axis.load()); + } + + friend class AnalogList; + +private: + std::shared_ptr<AnalogList> analog_list; + std::atomic<float> x_axis{0.0f}; + std::atomic<float> y_axis{0.0f}; +}; + +struct AnalogPair { + int analog_id; + Joystick* key_button; +}; + +class AnalogList { +public: + void AddButton(int analog_id, Joystick* key_button) { + std::lock_guard<std::mutex> guard(mutex); + list.push_back(AnalogPair{analog_id, key_button}); + } + + void RemoveButton(const Joystick* key_button) { + std::lock_guard<std::mutex> guard(mutex); + list.remove_if( + [key_button](const AnalogPair& pair) { return pair.key_button == key_button; }); + } + + bool ChangeJoystickStatus(int analog_id, float x, float y) { + std::lock_guard<std::mutex> guard(mutex); + bool button_found = false; + for (const AnalogPair& pair : list) { + if (pair.analog_id == analog_id) { + pair.key_button->x_axis.store(x); + pair.key_button->y_axis.store(y); + button_found = true; + } + } + return button_found; + } + +private: + std::mutex mutex; + std::list<AnalogPair> list; +}; + +AnalogFactory::AnalogFactory() : analog_list{std::make_shared<AnalogList>()} {} + +Joystick::~Joystick() { + analog_list->RemoveButton(this); +} + +ButtonFactory::ButtonFactory() + : button_list{std::make_shared<ButtonList>()}, analog_button_list{ + std::make_shared<AnalogButtonList>()} {} + +std::unique_ptr<Input::ButtonDevice> ButtonFactory::Create(const Common::ParamPackage& params) { + if (params.Has("axis")) { + const int axis_id = params.Get("axis", 0); + const float threshold = params.Get("threshold", 0.5f); + const std::string direction_name = params.Get("direction", ""); + bool trigger_if_greater; + if (direction_name == "+") { + trigger_if_greater = true; + } else if (direction_name == "-") { + trigger_if_greater = false; + } else { + trigger_if_greater = true; + LOG_ERROR(Input, "Unknown direction {}", direction_name); + } + std::unique_ptr<AnalogButton> analog_button = + std::make_unique<AnalogButton>(analog_button_list, threshold, trigger_if_greater); + analog_button_list->AddAnalogButton(axis_id, analog_button.get()); + return std::move(analog_button); + } + + int button_id = params.Get("code", 0); + std::unique_ptr<KeyButton> key_button = std::make_unique<KeyButton>(button_list); + button_list->AddButton(button_id, key_button.get()); + return std::move(key_button); +} + +bool ButtonFactory::PressKey(int button_id) { + return button_list->ChangeButtonStatus(button_id, true); +} + +bool ButtonFactory::ReleaseKey(int button_id) { + return button_list->ChangeButtonStatus(button_id, false); +} + +bool ButtonFactory::AnalogButtonEvent(int axis_id, float axis_val) { + return analog_button_list->ChangeAxisValue(axis_id, axis_val); +} + +std::unique_ptr<Input::AnalogDevice> AnalogFactory::Create(const Common::ParamPackage& params) { + int analog_id = params.Get("code", 0); + std::unique_ptr<Joystick> analog = std::make_unique<Joystick>(analog_list); + analog_list->AddButton(analog_id, analog.get()); + return std::move(analog); +} + +bool AnalogFactory::MoveJoystick(int analog_id, float x, float y) { + return analog_list->ChangeJoystickStatus(analog_id, x, y); +} + +ButtonFactory* ButtonHandler() { + return button.get(); +} + +AnalogFactory* AnalogHandler() { + return analog.get(); +} + +std::string GenerateButtonParamPackage(int button) { + Common::ParamPackage param{ + {"engine", "gamepad"}, + {"code", std::to_string(button)}, + }; + return param.Serialize(); +} + +std::string GenerateAnalogButtonParamPackage(int axis, float axis_val) { + Common::ParamPackage param{ + {"engine", "gamepad"}, + {"axis", std::to_string(axis)}, + }; + if (axis_val > 0) { + param.Set("direction", "+"); + param.Set("threshold", "0.5"); + } else { + param.Set("direction", "-"); + param.Set("threshold", "-0.5"); + } + + return param.Serialize(); +} + +std::string GenerateAnalogParamPackage(int axis_id) { + Common::ParamPackage param{ + {"engine", "gamepad"}, + {"code", std::to_string(axis_id)}, + }; + return param.Serialize(); +} + +NDKMotionFactory* NDKMotionHandler() { + return motion.get(); +} + +void Init() { + button = std::make_shared<ButtonFactory>(); + analog = std::make_shared<AnalogFactory>(); + motion = std::make_shared<NDKMotionFactory>(); + Input::RegisterFactory<Input::ButtonDevice>("gamepad", button); + Input::RegisterFactory<Input::AnalogDevice>("gamepad", analog); + Input::RegisterFactory<Input::MotionDevice>("motion_emu", motion); +} + +void Shutdown() { + Input::UnregisterFactory<Input::ButtonDevice>("gamepad"); + Input::UnregisterFactory<Input::AnalogDevice>("gamepad"); + Input::UnregisterFactory<Input::MotionDevice>("motion_emu"); + button.reset(); + analog.reset(); + motion.reset(); +} + +} // namespace InputManager diff --git a/src/android/app/src/main/jni/input_manager.h b/src/android/app/src/main/jni/input_manager.h new file mode 100644 index 000000000..637eeb67b --- /dev/null +++ b/src/android/app/src/main/jni/input_manager.h @@ -0,0 +1,141 @@ +// Copyright 2013 Dolphin Emulator Project / 2017 Citra Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include <map> +#include <memory> +#include <string> +#include "core/frontend/input.h" +#include "jni/ndk_motion.h" + +namespace InputManager { + +enum ButtonType { + // 3DS Controls + N3DS_BUTTON_A = 700, + N3DS_BUTTON_B = 701, + N3DS_BUTTON_X = 702, + N3DS_BUTTON_Y = 703, + N3DS_BUTTON_START = 704, + N3DS_BUTTON_SELECT = 705, + N3DS_BUTTON_HOME = 706, + N3DS_BUTTON_ZL = 707, + N3DS_BUTTON_ZR = 708, + N3DS_DPAD_UP = 709, + N3DS_DPAD_DOWN = 710, + N3DS_DPAD_LEFT = 711, + N3DS_DPAD_RIGHT = 712, + N3DS_CIRCLEPAD = 713, + N3DS_CIRCLEPAD_UP = 714, + N3DS_CIRCLEPAD_DOWN = 715, + N3DS_CIRCLEPAD_LEFT = 716, + N3DS_CIRCLEPAD_RIGHT = 717, + N3DS_STICK_C = 718, + N3DS_STICK_C_UP = 719, + N3DS_STICK_C_DOWN = 720, + N3DS_STICK_C_LEFT = 771, + N3DS_STICK_C_RIGHT = 772, + N3DS_TRIGGER_L = 773, + N3DS_TRIGGER_R = 774, + N3DS_BUTTON_DEBUG = 781, + N3DS_BUTTON_GPIO14 = 782 +}; + +class ButtonList; +class AnalogButtonList; +class AnalogList; + +/** + * A button device factory representing a gamepad. It receives input events and forward them + * to all button devices it created. + */ +class ButtonFactory final : public Input::Factory<Input::ButtonDevice> { +public: + ButtonFactory(); + + /** + * Creates a button device from a gamepad button + * @param params contains parameters for creating the device: + * - "code": the code of the key to bind with the button + */ + std::unique_ptr<Input::ButtonDevice> Create(const Common::ParamPackage& params) override; + + /** + * Sets the status of all buttons bound with the key to pressed + * @param key_code the code of the key to press + * @return whether the key event is consumed or not + */ + bool PressKey(int button_id); + + /** + * Sets the status of all buttons bound with the key to released + * @param key_code the code of the key to release + * @return whether the key event is consumed or not + */ + bool ReleaseKey(int button_id); + + /** + * Sets the status of all buttons bound with the key to released + * @param axis_id the code of the axis + * @param axis_val the value of the axis + * @return whether the key event is consumed or not + */ + bool AnalogButtonEvent(int axis_id, float axis_val); + + void ReleaseAllKeys(); + +private: + std::shared_ptr<ButtonList> button_list; + std::shared_ptr<AnalogButtonList> analog_button_list; +}; + +/** + * An analog device factory representing a gamepad(virtual or physical). It receives input events + * and forward them to all analog devices it created. + */ +class AnalogFactory final : public Input::Factory<Input::AnalogDevice> { +public: + AnalogFactory(); + + /** + * Creates an analog device from the gamepad joystick + * @param params contains parameters for creating the device: + * - "code": the code of the key to bind with the button + */ + std::unique_ptr<Input::AnalogDevice> Create(const Common::ParamPackage& params) override; + + /** + * Sets the status of all buttons bound with the key to pressed + * @param key_code the code of the analog stick + * @param x the x-axis value of the analog stick + * @param y the y-axis value of the analog stick + */ + bool MoveJoystick(int analog_id, float x, float y); + +private: + std::shared_ptr<AnalogList> analog_list; +}; + +/// Initializes and registers all built-in input device factories. +void Init(); + +/// Deregisters all built-in input device factories and shuts them down. +void Shutdown(); + +/// Gets the gamepad button device factory. +ButtonFactory* ButtonHandler(); + +/// Gets the gamepad analog device factory. +AnalogFactory* AnalogHandler(); + +/// Gets the NDK Motion device factory. +NDKMotionFactory* NDKMotionHandler(); + +std::string GenerateButtonParamPackage(int type); + +std::string GenerateAnalogButtonParamPackage(int axis, float axis_val); + +std::string GenerateAnalogParamPackage(int type); +} // namespace InputManager diff --git a/src/android/app/src/main/jni/mic.cpp b/src/android/app/src/main/jni/mic.cpp new file mode 100644 index 000000000..90191fbcf --- /dev/null +++ b/src/android/app/src/main/jni/mic.cpp @@ -0,0 +1,38 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <jni.h> +#include "common/logging/log.h" +#include "jni/id_cache.h" +#include "jni/mic.h" + +#ifdef HAVE_CUBEB +#include "audio_core/cubeb_input.h" +#endif + +namespace Mic { + +AndroidFactory::~AndroidFactory() = default; + +std::unique_ptr<Frontend::Mic::Interface> AndroidFactory::Create(std::string mic_device_name) { +#ifdef HAVE_CUBEB + if (!permission_granted) { + JNIEnv* env = IDCache::GetEnvForThread(); + permission_granted = env->CallStaticBooleanMethod(IDCache::GetNativeLibraryClass(), + IDCache::GetRequestMicPermission()); + } + + if (permission_granted) { + return std::make_unique<AudioCore::CubebInput>(std::move(mic_device_name)); + } else { + LOG_WARNING(Frontend, "Mic permissions denied"); + return std::make_unique<Frontend::Mic::NullMic>(); + } +#else + LOG_WARNING(Frontend, "No cubeb support"); + return std::make_unique<Frontend::Mic::NullMic>(); +#endif +} + +} // namespace Mic diff --git a/src/android/app/src/main/jni/mic.h b/src/android/app/src/main/jni/mic.h new file mode 100644 index 000000000..d790d52e5 --- /dev/null +++ b/src/android/app/src/main/jni/mic.h @@ -0,0 +1,21 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/frontend/mic.h" + +namespace Mic { + +class AndroidFactory final : public Frontend::Mic::RealMicFactory { +public: + ~AndroidFactory() override; + + std::unique_ptr<Frontend::Mic::Interface> Create(std::string mic_device_name) override; + +private: + bool permission_granted = false; +}; + +} // namespace Mic diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp new file mode 100644 index 000000000..9b7fb27a4 --- /dev/null +++ b/src/android/app/src/main/jni/native.cpp @@ -0,0 +1,728 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <algorithm> +#include <iostream> +#include <regex> +#include <thread> + +#include <android/native_window_jni.h> + +#include "audio_core/dsp_interface.h" +#include "common/file_util.h" +#include "common/logging/log.h" +#include "common/microprofile.h" +#include "common/scm_rev.h" +#include "common/scope_exit.h" +#include "common/string_util.h" +#include "core/core.h" +#include "core/frontend/applets/default_applets.h" +#include "core/frontend/camera/factory.h" +#include "core/frontend/mic.h" +#include "core/frontend/scope_acquire_context.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/nfc/nfc.h" +#include "core/savestate.h" +#include "core/settings.h" +#include "jni/applets/mii_selector.h" +#include "jni/applets/swkbd.h" +#include "jni/camera/ndk_camera.h" +#include "jni/camera/still_image_camera.h" +#include "jni/config.h" +#include "jni/emu_window/emu_window.h" +#include "jni/game_info.h" +#include "jni/game_settings.h" +#include "jni/id_cache.h" +#include "jni/input_manager.h" +#include "jni/mic.h" +#include "jni/native.h" +#include "jni/ndk_motion.h" +#include "video_core/renderer_base.h" +#include "video_core/renderer_opengl/texture_filters/texture_filterer.h" + +namespace { + +ANativeWindow* s_surf; + +std::unique_ptr<EmuWindow_Android> window; + +std::atomic<bool> stop_run{true}; +std::atomic<bool> pause_emulation{false}; + +std::mutex paused_mutex; +std::mutex running_mutex; +std::condition_variable running_cv; + +} // Anonymous namespace + +static std::string GetJString(JNIEnv* env, jstring jstr) { + if (!jstr) { + return {}; + } + + const char* s = env->GetStringUTFChars(jstr, nullptr); + std::string result = s; + env->ReleaseStringUTFChars(jstr, s); + return result; +} + +static bool DisplayAlertMessage(const char* caption, const char* text, bool yes_no) { + JNIEnv* env = IDCache::GetEnvForThread(); + + // Execute the Java method. + jboolean result = env->CallStaticBooleanMethod( + IDCache::GetNativeLibraryClass(), IDCache::GetDisplayAlertMsg(), env->NewStringUTF(caption), + env->NewStringUTF(text), yes_no ? JNI_TRUE : JNI_FALSE); + + return result != JNI_FALSE; +} + +static std::string DisplayAlertPrompt(const char* caption, const char* text, int buttonConfig) { + JNIEnv* env = IDCache::GetEnvForThread(); + + jstring value = reinterpret_cast<jstring>(env->CallStaticObjectMethod( + IDCache::GetNativeLibraryClass(), IDCache::GetDisplayAlertPrompt(), + env->NewStringUTF(caption), env->NewStringUTF(text), buttonConfig)); + + return GetJString(env, value); +} + +static int AlertPromptButton() { + JNIEnv* env = IDCache::GetEnvForThread(); + + // Execute the Java method. + return static_cast<int>(env->CallStaticIntMethod(IDCache::GetNativeLibraryClass(), + IDCache::GetAlertPromptButton())); +} + +static jobject ToJavaCoreError(Core::System::ResultStatus result) { + static const std::map<Core::System::ResultStatus, const char*> CoreErrorNameMap{ + {Core::System::ResultStatus::ErrorSystemFiles, "ErrorSystemFiles"}, + {Core::System::ResultStatus::ErrorSavestate, "ErrorSavestate"}, + {Core::System::ResultStatus::ErrorUnknown, "ErrorUnknown"}, + }; + + const auto name = CoreErrorNameMap.count(result) ? CoreErrorNameMap.at(result) : "ErrorUnknown"; + + JNIEnv* env = IDCache::GetEnvForThread(); + const jclass core_error_class = IDCache::GetCoreErrorClass(); + return env->GetStaticObjectField( + core_error_class, env->GetStaticFieldID(core_error_class, name, + "Lorg/citra/citra_emu/NativeLibrary$CoreError;")); +} + +static bool HandleCoreError(Core::System::ResultStatus result, const std::string& details) { + JNIEnv* env = IDCache::GetEnvForThread(); + return env->CallStaticBooleanMethod(IDCache::GetNativeLibraryClass(), IDCache::GetOnCoreError(), + ToJavaCoreError(result), + env->NewStringUTF(details.c_str())) != JNI_FALSE; +} + +static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max) { + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetDiskCacheProgressClass(), + IDCache::GetDiskCacheLoadProgress(), + IDCache::GetJavaLoadCallbackStage(stage), static_cast<jint>(progress), + static_cast<jint>(max)); +} + +static Camera::NDK::Factory* g_ndk_factory{}; + +static void TryShutdown() { + if (!window) { + return; + } + + window->DoneCurrent(); + Core::System::GetInstance().Shutdown(); + window.reset(); + InputManager::Shutdown(); + MicroProfileShutdown(); +} + +static Core::System::ResultStatus RunCitra(const std::string& filepath) { + // Citra core only supports a single running instance + std::lock_guard<std::mutex> lock(running_mutex); + + LOG_INFO(Frontend, "Citra is Starting"); + + MicroProfileOnThreadCreate("EmuThread"); + + if (filepath.empty()) { + LOG_CRITICAL(Frontend, "Failed to load ROM: No ROM specified"); + return Core::System::ResultStatus::ErrorLoader; + } + + window = std::make_unique<EmuWindow_Android>(s_surf); + + Core::System& system{Core::System::GetInstance()}; + + // Forces a config reload on game boot, if the user changed settings in the UI + Config{}; + // Replace with game-specific settings + u64 program_id{}; + FileUtil::SetCurrentRomPath(filepath); + auto app_loader = Loader::GetLoader(filepath); + if (app_loader) { + app_loader->ReadProgramId(program_id); + GameSettings::LoadOverrides(program_id); + } + Settings::Apply(); + + Camera::RegisterFactory("image", std::make_unique<Camera::StillImage::Factory>()); + + auto ndk_factory = std::make_unique<Camera::NDK::Factory>(); + g_ndk_factory = ndk_factory.get(); + Camera::RegisterFactory("ndk", std::move(ndk_factory)); + + // Register frontend applets + Frontend::RegisterDefaultApplets(); + system.RegisterMiiSelector(std::make_shared<MiiSelector::AndroidMiiSelector>()); + system.RegisterSoftwareKeyboard(std::make_shared<SoftwareKeyboard::AndroidKeyboard>()); + + // Register real Mic factory + Frontend::Mic::RegisterRealMicFactory(std::make_unique<Mic::AndroidFactory>()); + + InputManager::Init(); + + window->MakeCurrent(); + const Core::System::ResultStatus load_result{system.Load(*window, filepath)}; + if (load_result != Core::System::ResultStatus::Success) { + return load_result; + } + + auto& telemetry_session = Core::System::GetInstance().TelemetrySession(); + telemetry_session.AddField(Common::Telemetry::FieldType::App, "Frontend", "SDL"); + + stop_run = false; + pause_emulation = false; + + LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0); + + std::unique_ptr<Frontend::GraphicsContext> cpu_context; + if (Settings::values.use_asynchronous_gpu_emulation) { + cpu_context = window->CreateSharedContext(); + cpu_context->MakeCurrent(); + } + + system.Renderer().Rasterizer()->LoadDiskResources(stop_run, &LoadDiskCacheProgress); + + if (Settings::values.use_asynchronous_gpu_emulation) { + cpu_context->DoneCurrent(); + cpu_context.reset(); + } + + LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0); + + SCOPE_EXIT({ TryShutdown(); }); + + // Audio stretching on Android is only useful with lower framerates, disable it when fullspeed + Core::TimingEventType* audio_stretching_event{}; + const s64 audio_stretching_ticks{msToCycles(500)}; + audio_stretching_event = + system.CoreTiming().RegisterEvent("AudioStretchingEvent", [&](u64, s64 cycles_late) { + if (Settings::values.enable_audio_stretching) { + Core::DSP().EnableStretching( + Core::System::GetInstance().GetAndResetPerfStats().emulation_speed < 0.95); + } + + system.CoreTiming().ScheduleEvent(audio_stretching_ticks - cycles_late, + audio_stretching_event); + }); + system.CoreTiming().ScheduleEvent(audio_stretching_ticks, audio_stretching_event); + + // Start running emulation + while (!stop_run) { + if (!pause_emulation) { + const auto result = system.RunLoop(); + if (result == Core::System::ResultStatus::Success) { + continue; + } + if (result == Core::System::ResultStatus::ShutdownRequested) { + return result; // This also exits the emulation activity + } else { + InputManager::NDKMotionHandler()->DisableSensors(); + if (!HandleCoreError(result, system.GetStatusDetails())) { + // Frontend requests us to abort + return result; + } + InputManager::NDKMotionHandler()->EnableSensors(); + } + } else { + // Ensure no audio bleeds out while game is paused + const float volume = Settings::values.volume; + SCOPE_EXIT({ Settings::values.volume = volume; }); + Settings::values.volume = 0; + + std::unique_lock<std::mutex> pause_lock(paused_mutex); + running_cv.wait(pause_lock, [] { return !pause_emulation || stop_run; }); + window->PollEvents(); + } + } + + return Core::System::ResultStatus::Success; +} + +extern "C" { + +void Java_org_citra_citra_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jobject surf) { + s_surf = ANativeWindow_fromSurface(env, surf); + + if (window) { + window->OnSurfaceChanged(s_surf); + } + + LOG_INFO(Frontend, "Surface changed"); +} + +void Java_org_citra_citra_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, + [[maybe_unused]] jclass clazz) { + ANativeWindow_release(s_surf); + s_surf = nullptr; + if (window) { + window->OnSurfaceChanged(s_surf); + } +} + +void Java_org_citra_citra_1emu_NativeLibrary_DoFrame(JNIEnv* env, [[maybe_unused]] jclass clazz) { + if (stop_run || pause_emulation) { + return; + } + window->TryPresenting(); +} + +void Java_org_citra_citra_1emu_NativeLibrary_NotifyOrientationChange(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jint layout_option, + jint rotation) { + Settings::values.layout_option = static_cast<Settings::LayoutOption>(layout_option); + if (VideoCore::g_renderer) { + VideoCore::g_renderer->UpdateCurrentFramebufferLayout(!(rotation % 2)); + } + InputManager::screen_rotation = rotation; + Camera::NDK::g_rotation = rotation; +} + +void Java_org_citra_citra_1emu_NativeLibrary_SwapScreens(JNIEnv* env, [[maybe_unused]] jclass clazz, + jboolean swap_screens, jint rotation) { + Settings::values.swap_screen = swap_screens; + if (VideoCore::g_renderer) { + VideoCore::g_renderer->UpdateCurrentFramebufferLayout(!(rotation % 2)); + } + InputManager::screen_rotation = rotation; + Camera::NDK::g_rotation = rotation; +} + +void Java_org_citra_citra_1emu_NativeLibrary_SetUserDirectory(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_directory) { + FileUtil::SetCurrentDir(GetJString(env, j_directory)); +} + +jobjectArray Java_org_citra_citra_1emu_NativeLibrary_GetInstalledGamePaths( + JNIEnv* env, [[maybe_unused]] jclass clazz) { + std::vector<std::string> games; + const FileUtil::DirectoryEntryCallable ScanDir = + [&games, &ScanDir](u64*, const std::string& directory, const std::string& virtual_name) { + std::string path = directory + virtual_name; + if (FileUtil::IsDirectory(path)) { + path += '/'; + FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir); + } else { + auto loader = Loader::GetLoader(path); + if (loader) { + bool executable{}; + const Loader::ResultStatus result = loader->IsExecutable(executable); + if (Loader::ResultStatus::Success == result && executable) { + games.emplace_back(path); + } + } + } + return true; + }; + ScanDir(nullptr, "", + FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + + "Nintendo " + "3DS/00000000000000000000000000000000/" + "00000000000000000000000000000000/title/00040000"); + ScanDir(nullptr, "", + FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "00000000000000000000000000000000/title/00040010"); + jobjectArray jgames = env->NewObjectArray(static_cast<jsize>(games.size()), + env->FindClass("java/lang/String"), nullptr); + for (jsize i = 0; i < games.size(); ++i) + env->SetObjectArrayElement(jgames, i, env->NewStringUTF(games[i].c_str())); + return jgames; +} + +// TODO(xperia64): ensure these cannot be called in an invalid state (e.g. after StopEmulation) +void Java_org_citra_citra_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env, + [[maybe_unused]] jclass clazz) { + pause_emulation = false; + running_cv.notify_all(); + InputManager::NDKMotionHandler()->EnableSensors(); +} + +void Java_org_citra_citra_1emu_NativeLibrary_PauseEmulation(JNIEnv* env, + [[maybe_unused]] jclass clazz) { + pause_emulation = true; + InputManager::NDKMotionHandler()->DisableSensors(); +} + +void Java_org_citra_citra_1emu_NativeLibrary_StopEmulation(JNIEnv* env, + [[maybe_unused]] jclass clazz) { + stop_run = true; + pause_emulation = false; + window->StopPresenting(); + running_cv.notify_all(); +} + +jboolean Java_org_citra_citra_1emu_NativeLibrary_IsRunning(JNIEnv* env, + [[maybe_unused]] jclass clazz) { + return static_cast<jboolean>(!stop_run); +} + +jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_device, jint j_button, + jint action) { + bool consumed{}; + if (action) { + consumed = InputManager::ButtonHandler()->PressKey(j_button); + } else { + consumed = InputManager::ButtonHandler()->ReleaseKey(j_button); + } + + return static_cast<jboolean>(consumed); +} + +jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_device, jint axis, + jfloat x, jfloat y) { + // Clamp joystick movement to supported minimum and maximum + // Citra uses an inverted y axis sent by the frontend + x = std::clamp(x, -1.f, 1.f); + y = std::clamp(-y, -1.f, 1.f); + + // Clamp the input to a circle (while touch input is already clamped in the frontend, gamepad is + // unknown) + float r = x * x + y * y; + if (r > 1.0f) { + r = std::sqrt(r); + x /= r; + y /= r; + } + return static_cast<jboolean>(InputManager::AnalogHandler()->MoveJoystick(axis, x, y)); +} + +jboolean Java_org_citra_citra_1emu_NativeLibrary_onGamePadAxisEvent(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_device, jint axis_id, + jfloat axis_val) { + return static_cast<jboolean>( + InputManager::ButtonHandler()->AnalogButtonEvent(axis_id, axis_val)); +} + +jboolean Java_org_citra_citra_1emu_NativeLibrary_onTouchEvent(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jfloat x, jfloat y, + jboolean pressed) { + return static_cast<jboolean>( + window->OnTouchEvent(static_cast<int>(x + 0.5), static_cast<int>(y + 0.5), pressed)); +} + +void Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, + [[maybe_unused]] jclass clazz, jfloat x, + jfloat y) { + window->OnTouchMoved((int)x, (int)y); +} + +jintArray Java_org_citra_citra_1emu_NativeLibrary_GetIcon(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_file) { + std::string filepath = GetJString(env, j_file); + + std::vector<u16> icon_data = GameInfo::GetIcon(filepath); + if (icon_data.size() == 0) { + return 0; + } + + jintArray icon = env->NewIntArray(static_cast<jsize>(icon_data.size() / 2)); + env->SetIntArrayRegion(icon, 0, env->GetArrayLength(icon), + reinterpret_cast<jint*>(icon_data.data())); + + return icon; +} + +jstring Java_org_citra_citra_1emu_NativeLibrary_GetTitle(JNIEnv* env, [[maybe_unused]] jclass clazz, + jstring j_filename) { + std::string filepath = GetJString(env, j_filename); + auto Title = GameInfo::GetTitle(filepath); + return env->NewStringUTF(Common::UTF16ToUTF8(Title).data()); +} + +jstring Java_org_citra_citra_1emu_NativeLibrary_GetDescription(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_filename) { + return j_filename; +} + +jstring Java_org_citra_citra_1emu_NativeLibrary_GetGameId(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_filename) { + return j_filename; +} + +jstring Java_org_citra_citra_1emu_NativeLibrary_GetRegions(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_filename) { + std::string filepath = GetJString(env, j_filename); + + std::string regions = GameInfo::GetRegions(filepath); + + return env->NewStringUTF(regions.c_str()); +} + +jstring Java_org_citra_citra_1emu_NativeLibrary_GetCompany(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_filename) { + std::string filepath = GetJString(env, j_filename); + auto publisher = GameInfo::GetPublisher(filepath); + return env->NewStringUTF(Common::UTF16ToUTF8(publisher).data()); +} + +jstring Java_org_citra_citra_1emu_NativeLibrary_GetGitRevision(JNIEnv* env, + [[maybe_unused]] jclass clazz) { + return nullptr; +} + +void Java_org_citra_citra_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env, + [[maybe_unused]] jclass clazz) { + Config{}; +} + +jint Java_org_citra_citra_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env, + [[maybe_unused]] jclass clazz) { + return 0; +} + +void Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z( + JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_file, jstring j_savestate, + jboolean j_delete_savestate) {} + +void Java_org_citra_citra_1emu_NativeLibrary_ReloadSettings(JNIEnv* env, + [[maybe_unused]] jclass clazz) { + Config{}; + Core::System& system{Core::System::GetInstance()}; + + // Replace with game-specific settings + if (system.IsPoweredOn()) { + u64 program_id{}; + system.GetAppLoader().ReadProgramId(program_id); + GameSettings::LoadOverrides(program_id); + } + + Settings::Apply(); +} + +jstring Java_org_citra_citra_1emu_NativeLibrary_GetUserSetting(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_game_id, jstring j_section, + jstring j_key) { + std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); + std::string_view section = env->GetStringUTFChars(j_section, 0); + std::string_view key = env->GetStringUTFChars(j_key, 0); + + // TODO + + env->ReleaseStringUTFChars(j_game_id, game_id.data()); + env->ReleaseStringUTFChars(j_section, section.data()); + env->ReleaseStringUTFChars(j_key, key.data()); + + return env->NewStringUTF(""); +} + +void Java_org_citra_citra_1emu_NativeLibrary_SetUserSetting(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_game_id, jstring j_section, + jstring j_key, jstring j_value) { + std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); + std::string_view section = env->GetStringUTFChars(j_section, 0); + std::string_view key = env->GetStringUTFChars(j_key, 0); + std::string_view value = env->GetStringUTFChars(j_value, 0); + + // TODO + + env->ReleaseStringUTFChars(j_game_id, game_id.data()); + env->ReleaseStringUTFChars(j_section, section.data()); + env->ReleaseStringUTFChars(j_key, key.data()); + env->ReleaseStringUTFChars(j_value, value.data()); +} + +void Java_org_citra_citra_1emu_NativeLibrary_InitGameIni(JNIEnv* env, [[maybe_unused]] jclass clazz, + jstring j_game_id) { + std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); + + // TODO + + env->ReleaseStringUTFChars(j_game_id, game_id.data()); +} + +jdoubleArray Java_org_citra_citra_1emu_NativeLibrary_GetPerfStats(JNIEnv* env, + [[maybe_unused]] jclass clazz) { + auto& core = Core::System::GetInstance(); + jdoubleArray j_stats = env->NewDoubleArray(4); + + if (core.IsPoweredOn()) { + auto results = core.GetAndResetPerfStats(); + + // Converting the structure into an array makes it easier to pass it to the frontend + double stats[4] = {results.system_fps, results.game_fps, results.frametime, + results.emulation_speed}; + + env->SetDoubleArrayRegion(j_stats, 0, 4, stats); + } + + return j_stats; +} + +void Java_org_citra_citra_1emu_utils_DirectoryInitialization_SetSysDirectory( + JNIEnv* env, [[maybe_unused]] jclass clazz, jstring j_path) { + std::string_view path = env->GetStringUTFChars(j_path, 0); + + env->ReleaseStringUTFChars(j_path, path.data()); +} + +void Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_path) { + const std::string path = GetJString(env, j_path); + + if (!stop_run) { + stop_run = true; + running_cv.notify_all(); + } + + const Core::System::ResultStatus result{RunCitra(path)}; + if (result != Core::System::ResultStatus::Success) { + env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), + IDCache::GetExitEmulationActivity(), static_cast<int>(result)); + } +} + +jobjectArray Java_org_citra_citra_1emu_NativeLibrary_GetTextureFilterNames(JNIEnv* env, + jclass clazz) { + auto names = OpenGL::TextureFilterer::GetFilterNames(); + jobjectArray ret = (jobjectArray)env->NewObjectArray(static_cast<jsize>(names.size()), + env->FindClass("java/lang/String"), + env->NewStringUTF("")); + for (jsize i = 0; i < names.size(); ++i) + env->SetObjectArrayElement(ret, i, env->NewStringUTF(names[i].data())); + return ret; +} + +void Java_org_citra_citra_1emu_NativeLibrary_ReloadCameraDevices(JNIEnv* env, jclass clazz) { + if (g_ndk_factory) { + g_ndk_factory->ReloadCameraDevices(); + } +} + +jboolean Java_org_citra_citra_1emu_NativeLibrary_LoadAmiibo(JNIEnv* env, jclass clazz, + jbyteArray bytes) { + Core::System& system{Core::System::GetInstance()}; + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u"); + if (nfc == nullptr || env->GetArrayLength(bytes) != sizeof(Service::NFC::AmiiboData)) { + return static_cast<jboolean>(false); + } + + Service::NFC::AmiiboData amiibo_data{}; + env->GetByteArrayRegion(bytes, 0, sizeof(Service::NFC::AmiiboData), + reinterpret_cast<jbyte*>(&amiibo_data)); + + nfc->LoadAmiibo(amiibo_data); + return static_cast<jboolean>(true); +} + +void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv* env, jclass clazz) { + Core::System& system{Core::System::GetInstance()}; + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService<Service::NFC::Module::Interface>("nfc:u"); + if (nfc == nullptr) { + return; + } + + nfc->RemoveAmiibo(); +} + +void Java_org_citra_citra_1emu_NativeLibrary_InstallCIAS(JNIEnv* env, [[maybe_unused]] jclass clazz, + jobjectArray path) { + const jsize count{env->GetArrayLength(path)}; + std::vector<std::string> paths; + for (jsize idx{0}; idx < count; ++idx) { + paths.emplace_back( + GetJString(env, static_cast<jstring>(env->GetObjectArrayElement(path, idx)))); + } + std::atomic<jsize> idx{count}; + std::vector<std::thread> threads; + std::generate_n(std::back_inserter(threads), + std::min<jsize>(std::thread::hardware_concurrency(), count), [&] { + return std::thread{[&idx, &paths, env] { + jsize work_idx; + while ((work_idx = --idx) >= 0) { + LOG_INFO(Frontend, "Installing CIA {}", work_idx); + Service::AM::InstallCIA(paths[work_idx]); + } + }}; + }); + for (auto& thread : threads) + thread.join(); +} + +jobjectArray Java_org_citra_citra_1emu_NativeLibrary_GetSavestateInfo( + JNIEnv* env, [[maybe_unused]] jclass clazz) { + const jclass date_class = env->FindClass("java/util/Date"); + const auto date_constructor = env->GetMethodID(date_class, "<init>", "(J)V"); + + const jclass savestate_info_class = IDCache::GetSavestateInfoClass(); + const auto slot_field = env->GetFieldID(savestate_info_class, "slot", "I"); + const auto date_field = env->GetFieldID(savestate_info_class, "time", "Ljava/util/Date;"); + + const Core::System& system{Core::System::GetInstance()}; + if (!system.IsPoweredOn()) { + return nullptr; + } + + u64 title_id; + if (system.GetAppLoader().ReadProgramId(title_id) != Loader::ResultStatus::Success) { + return nullptr; + } + + const auto savestates = Core::ListSaveStates(title_id); + const jobjectArray array = + env->NewObjectArray(static_cast<jsize>(savestates.size()), savestate_info_class, nullptr); + for (std::size_t i = 0; i < savestates.size(); ++i) { + const jobject object = env->AllocObject(savestate_info_class); + env->SetIntField(object, slot_field, static_cast<jint>(savestates[i].slot)); + env->SetObjectField(object, date_field, + env->NewObject(date_class, date_constructor, + static_cast<jlong>(savestates[i].time * 1000))); + + env->SetObjectArrayElement(array, i, object); + } + return array; +} + +void Java_org_citra_citra_1emu_NativeLibrary_SaveState(JNIEnv* env, jclass clazz, jint slot) { + Core::System::GetInstance().SendSignal(Core::System::Signal::Save, slot); +} + +void Java_org_citra_citra_1emu_NativeLibrary_LoadState(JNIEnv* env, jclass clazz, jint slot) { + Core::System::GetInstance().SendSignal(Core::System::Signal::Load, slot); +} + +} // extern "C" diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h new file mode 100644 index 000000000..f02b15b35 --- /dev/null +++ b/src/android/app/src/main/jni/native.h @@ -0,0 +1,160 @@ +// Copyright 2019 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <jni.h> + +// Function calls from the Java side +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env, + jclass clazz); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_PauseEmulation(JNIEnv* env, + jclass clazz); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_StopEmulation(JNIEnv* env, + jclass clazz); + +JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_IsRunning(JNIEnv* env, + jclass clazz); + +JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onGamePadEvent( + JNIEnv* env, jclass clazz, jstring j_device, jint j_button, jint action); + +JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onGamePadMoveEvent( + JNIEnv* env, jclass clazz, jstring j_device, jint axis, jfloat x, jfloat y); + +JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onGamePadAxisEvent( + JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val); + +JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_NativeLibrary_onTouchEvent(JNIEnv* env, + jclass clazz, + jfloat x, jfloat y, + jboolean pressed); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, + jclass clazz, jfloat x, + jfloat y); + +JNIEXPORT jintArray JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetIcon(JNIEnv* env, + jclass clazz, + jstring j_file); + +JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetTitle(JNIEnv* env, + jclass clazz, + jstring j_filename); + +JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetDescription( + JNIEnv* env, jclass clazz, jstring j_filename); + +JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetGameId(JNIEnv* env, + jclass clazz, + jstring j_filename); + +JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetRegions(JNIEnv* env, + jclass clazz, + jstring j_filename); + +JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetCompany(JNIEnv* env, + jclass clazz, + jstring j_filename); + +JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetGitRevision(JNIEnv* env, + jclass clazz); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetUserDirectory( + JNIEnv* env, jclass clazz, jstring j_directory); + +JNIEXPORT jobjectArray JNICALL +Java_org_citra_citra_1emu_NativeLibrary_GetInstalledGamePaths(JNIEnv* env, jclass clazz); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_utils_DirectoryInitialization_SetSysDirectory( + JNIEnv* env, jclass clazz, jstring path_); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env, + jclass clazz, + jstring path); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env, + jclass clazz); + +JNIEXPORT jint JNICALL Java_org_citra_citra_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env, + jclass clazz); +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetProfiling(JNIEnv* env, + jclass clazz, + jboolean enable); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env, + jclass clazz); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_NotifyOrientationChange( + JNIEnv* env, jclass clazz, jint layout_option, jint rotation); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SwapScreens(JNIEnv* env, + jclass clazz, + jboolean swap_screens, + jint rotation); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2( + JNIEnv* env, jclass clazz, jstring j_path); + +JNIEXPORT void JNICALL +Java_org_citra_citra_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z( + JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env, + jclass clazz, + jobject surf); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, + jclass clazz); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_InitGameIni(JNIEnv* env, + jclass clazz, + jstring j_game_id); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_ReloadSettings(JNIEnv* env, + jclass clazz); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SetUserSetting( + JNIEnv* env, jclass clazz, jstring j_game_id, jstring j_section, jstring j_key, + jstring j_value); + +JNIEXPORT jstring JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetUserSetting( + JNIEnv* env, jclass clazz, jstring game_id, jstring section, jstring key); + +JNIEXPORT jdoubleArray JNICALL Java_org_citra_citra_1emu_NativeLibrary_GetPerfStats(JNIEnv* env, + jclass clazz); + +JNIEXPORT jobjectArray JNICALL +Java_org_citra_citra_1emu_NativeLibrary_GetTextureFilterNames(JNIEnv* env, jclass clazz); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_ReloadCameraDevices(JNIEnv* env, + jclass clazz); + +JNIEXPORT jboolean Java_org_citra_citra_1emu_NativeLibrary_LoadAmiibo(JNIEnv* env, jclass clazz, + jbyteArray bytes); + +JNIEXPORT void Java_org_citra_citra_1emu_NativeLibrary_RemoveAmiibo(JNIEnv* env, jclass clazz); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_InstallCIAS(JNIEnv* env, + jclass clazz, + jobjectArray path); + +JNIEXPORT jobjectArray JNICALL +Java_org_citra_citra_1emu_NativeLibrary_GetSavestateInfo(JNIEnv* env, jclass clazz); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_SaveState(JNIEnv* env, jclass clazz, + jint slot); + +JNIEXPORT void JNICALL Java_org_citra_citra_1emu_NativeLibrary_LoadState(JNIEnv* env, jclass clazz, + jint slot); + +#ifdef __cplusplus +} +#endif diff --git a/src/android/app/src/main/jni/ndk_motion.cpp b/src/android/app/src/main/jni/ndk_motion.cpp new file mode 100644 index 000000000..0eab444a9 --- /dev/null +++ b/src/android/app/src/main/jni/ndk_motion.cpp @@ -0,0 +1,196 @@ +#include <chrono> +#include <thread> + +#include <android/sensor.h> + +#include "common/assert.h" +#include "common/logging/log.h" +#include "common/vector_math.h" +#include "jni/native.h" +#include "jni/ndk_motion.h" + +namespace InputManager { + +namespace { +using Common::Vec3; +} + +class NDKMotion final : public Input::MotionDevice { + std::chrono::microseconds update_period; + + ASensorManager* sensor_manager = nullptr; + ALooper* looper = nullptr; + ASensorEventQueue* event_queue; + + mutable std::atomic<Vec3<float>> acceleration{}; + mutable std::atomic<Vec3<float>> rotation{}; + static_assert(decltype(acceleration)::is_always_lock_free, "vectors are not lock free"); + std::thread poll_thread; + std::atomic<bool> stop_polling = false; + + static Vec3<float> TransformAxes(Vec3<float> in) { + // 3DS Y+ Phone Z+ + // on | laying | + // table | in | + // |_______ X- portrait |_______ X+ + // / mode / + // / / + // Z- Y- + Vec3<float> out; + out.y = in.z; + // rotations are 90 degrees counter-clockwise from portrait + switch (screen_rotation) { + case 0: + out.x = -in.x; + out.z = in.y; + break; + case 1: + out.x = in.y; + out.z = in.x; + break; + case 2: + out.x = in.x; + out.z = -in.y; + break; + case 3: + out.x = -in.y; + out.z = -in.x; + break; + default: + UNREACHABLE(); + } + return out; + } + + void Construct() { + sensor_manager = ASensorManager_getInstanceForPackage("org.citra.citra_emu"); + looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS); + if (!sensor_manager || !looper) { + LOG_CRITICAL(Input, "Could not retrieve sensor manager"); + return; + } + event_queue = ASensorManager_createEventQueue(sensor_manager, looper, 0, nullptr, nullptr); + if (!event_queue) { + LOG_ERROR(Input, "Could not create sensor event queue"); + return; + } + + EnableSensors(); + } + + void Destruct() { + ASensorManager_destroyEventQueue(sensor_manager, event_queue); + } + + void Update() const { + ALooper_pollAll(0, nullptr, nullptr, nullptr); + ASensorEvent event{}; + std::optional<Vec3<float>> new_accel{}, new_rot{}; + while (ASensorEventQueue_getEvents(event_queue, &event, 1) > 0) { + if (event.type == ASENSOR_TYPE_ACCELEROMETER) { + new_accel.emplace(event.vector.x, event.vector.y, event.vector.z); + } else if (event.type == ASENSOR_TYPE_GYROSCOPE) { + new_rot.emplace(event.vector.x, event.vector.y, event.vector.z); + } + // occasionally the queue has ASENSOR_TYPE_ADDITIONAL_INFO events + // but so far there is no reason to handle them + } + if (new_accel) { + // convert from m/(s^2) to g and invert + acceleration = TransformAxes(*new_accel) / -ASENSOR_STANDARD_GRAVITY; + } + if (new_rot) { + // convert from rad/s to deg/s + rotation = TransformAxes(*new_rot) * 180.0f / static_cast<float>(M_PI); + } + } + +public: + NDKMotion(std::chrono::microseconds update_period_, bool asynchronous = false) + : update_period(update_period_) { + if (asynchronous) { + poll_thread = std::thread([this] { + Construct(); + auto start = std::chrono::high_resolution_clock::now(); + while (!stop_polling) { + Update(); + std::this_thread::sleep_until(start += update_period); + } + Destruct(); + }); + } else { + Construct(); + } + } + + ~NDKMotion() { + if (std::thread::id{} == poll_thread.get_id()) { + Destruct(); + } else { + stop_polling = true; + poll_thread.join(); + } + } + + std::tuple<Vec3<float>, Vec3<float>> GetStatus() const override { + if (std::thread::id{} == poll_thread.get_id()) { + Update(); + } + return {acceleration, rotation}; + } + + void EnableSensors() { + const auto init_sensor = [this](int sensor_type) { + ASensorRef sensor = ASensorManager_getDefaultSensor(sensor_manager, sensor_type); + if (!sensor) { + LOG_ERROR(Input, "Could not find sensor of type {}", sensor_type); + return; + } + int error = ASensorEventQueue_registerSensor( + event_queue, sensor, + std::max(ASensor_getMinDelay(sensor), static_cast<int>(update_period.count())), 0); + if (error < 0) + LOG_ERROR(Input, "Registering sensor returned error code {}", error); + }; + + LOG_TRACE(Input, "Enabling sensors.."); + init_sensor(ASENSOR_TYPE_ACCELEROMETER); + init_sensor(ASENSOR_TYPE_GYROSCOPE); + } + + void DisableSensors() { + const auto disable_sensor = [this](int sensor_type) { + ASensorRef sensor = ASensorManager_getDefaultSensor(sensor_manager, sensor_type); + if (!sensor) { + LOG_ERROR(Input, "Could not find sensor of type {}", sensor_type); + return; + } + int error = ASensorEventQueue_disableSensor(event_queue, sensor); + if (error < 0) + LOG_ERROR(Input, "Disabling sensor returned error code {}", error); + }; + + LOG_TRACE(Input, "Disabling sensors.."); + disable_sensor(ASENSOR_TYPE_ACCELEROMETER); + disable_sensor(ASENSOR_TYPE_GYROSCOPE); + } +}; + +std::unique_ptr<Input::MotionDevice> NDKMotionFactory::Create(const Common::ParamPackage& params) { + std::chrono::milliseconds update_period{params.Get("update_period", 4)}; + std::unique_ptr<NDKMotion> ndk_motion = std::make_unique<NDKMotion>(update_period); + ndk_motion_device = ndk_motion.get(); + return std::move(ndk_motion); +} + +void NDKMotionFactory::EnableSensors() { + if (ndk_motion_device) + ndk_motion_device->EnableSensors(); +} + +void NDKMotionFactory::DisableSensors() { + if (ndk_motion_device) + ndk_motion_device->DisableSensors(); +} + +} // namespace InputManager diff --git a/src/android/app/src/main/jni/ndk_motion.h b/src/android/app/src/main/jni/ndk_motion.h new file mode 100644 index 000000000..01a8f8d9c --- /dev/null +++ b/src/android/app/src/main/jni/ndk_motion.h @@ -0,0 +1,28 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include "core/frontend/input.h" + +namespace InputManager { + +inline std::atomic<int> screen_rotation; + +class NDKMotion; + +class NDKMotionFactory final : public Input::Factory<Input::MotionDevice> { +public: + /** + * Creates a motion device that obtains data from device sensors + */ + std::unique_ptr<Input::MotionDevice> Create(const Common::ParamPackage& params) override; + + void EnableSensors(); + void DisableSensors(); + +private: + NDKMotion* ndk_motion_device; +}; +} // namespace InputManager diff --git a/src/android/app/src/main/res/animator/settings_enter.xml b/src/android/app/src/main/res/animator/settings_enter.xml new file mode 100644 index 000000000..3c216a054 --- /dev/null +++ b/src/android/app/src/main/res/animator/settings_enter.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="yFraction" + android:startOffset="@android:integer/config_shortAnimTime" + android:valueFrom="1.0" + android:valueTo="0" /> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="translationZ" + android:startOffset="@android:integer/config_shortAnimTime" + android:valueFrom="100.0" + android:valueTo="0" /> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="elevation" + android:startOffset="@android:integer/config_shortAnimTime" + android:valueFrom="100.0" + android:valueTo="0" /> + +</set> \ No newline at end of file diff --git a/src/android/app/src/main/res/animator/settings_exit.xml b/src/android/app/src/main/res/animator/settings_exit.xml new file mode 100644 index 000000000..a233b6757 --- /dev/null +++ b/src/android/app/src/main/res/animator/settings_exit.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/accelerate_cubic" + android:propertyName="visibleness" + android:valueFrom="1.0f" + android:valueTo="0.6f" + android:valueType="floatType" /> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="translationZ" + android:startOffset="@android:integer/config_shortAnimTime" + android:valueFrom="0" + android:valueTo="-100.0" /> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="elevation" + android:startOffset="@android:integer/config_shortAnimTime" + android:valueFrom="0" + android:valueTo="-100.0" /> + +</set> \ No newline at end of file diff --git a/src/android/app/src/main/res/animator/settings_pop_enter.xml b/src/android/app/src/main/res/animator/settings_pop_enter.xml new file mode 100644 index 000000000..080bc27c4 --- /dev/null +++ b/src/android/app/src/main/res/animator/settings_pop_enter.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="visibleness" + android:valueFrom="0.6f" + android:valueTo="1.0f" + android:valueType="floatType" /> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="translationZ" + android:startOffset="@android:integer/config_shortAnimTime" + android:valueFrom="-100.0" + android:valueTo="0" /> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="elevation" + android:startOffset="@android:integer/config_shortAnimTime" + android:valueFrom="-100.0" + android:valueTo="0" /> + +</set> \ No newline at end of file diff --git a/src/android/app/src/main/res/animator/setttings_pop_exit.xml b/src/android/app/src/main/res/animator/setttings_pop_exit.xml new file mode 100644 index 000000000..4fccbcca2 --- /dev/null +++ b/src/android/app/src/main/res/animator/setttings_pop_exit.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/accelerate_cubic" + android:propertyName="yFraction" + android:valueFrom="0" + android:valueTo="1.0" /> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="translationZ" + android:startOffset="@android:integer/config_shortAnimTime" + android:valueFrom="0.0" + android:valueTo="100" /> + + <objectAnimator + android:duration="@android:integer/config_mediumAnimTime" + android:interpolator="@android:interpolator/decelerate_cubic" + android:propertyName="elevation" + android:startOffset="@android:integer/config_shortAnimTime" + android:valueFrom="0.0" + android:valueTo="100" /> + +</set> \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable-hdpi/button_a.png b/src/android/app/src/main/res/drawable-hdpi/button_a.png new file mode 100644 index 000000000..f96a2061e Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_a.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_a_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_a_pressed.png new file mode 100644 index 000000000..785a258ee Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_a_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_b.png b/src/android/app/src/main/res/drawable-hdpi/button_b.png new file mode 100644 index 000000000..b15d2b549 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_b.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_b_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_b_pressed.png new file mode 100644 index 000000000..b11d5fcee Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_b_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_l.png b/src/android/app/src/main/res/drawable-hdpi/button_l.png new file mode 100644 index 000000000..e19469a7b Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_l.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_l_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_l_pressed.png new file mode 100644 index 000000000..280857f64 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_l_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_r.png b/src/android/app/src/main/res/drawable-hdpi/button_r.png new file mode 100644 index 000000000..f72cdc1dc Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_r.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_r_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_r_pressed.png new file mode 100644 index 000000000..c47d34253 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_r_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_select.png b/src/android/app/src/main/res/drawable-hdpi/button_select.png new file mode 100644 index 000000000..6961b88d2 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_select.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_select_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_select_pressed.png new file mode 100644 index 000000000..8ee471419 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_select_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_start.png b/src/android/app/src/main/res/drawable-hdpi/button_start.png new file mode 100644 index 000000000..72856cf47 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_start.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_start_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_start_pressed.png new file mode 100644 index 000000000..f96cd3359 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_start_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_x.png b/src/android/app/src/main/res/drawable-hdpi/button_x.png new file mode 100644 index 000000000..1a0fd1924 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_x.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_x_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_x_pressed.png new file mode 100644 index 000000000..089cb3af1 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_x_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_y.png b/src/android/app/src/main/res/drawable-hdpi/button_y.png new file mode 100644 index 000000000..bc22680c4 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_y.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_y_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_y_pressed.png new file mode 100644 index 000000000..6e9e89ec9 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_y_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_zl.png b/src/android/app/src/main/res/drawable-hdpi/button_zl.png new file mode 100644 index 000000000..dd5d4d5b3 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_zl.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_zl_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_zl_pressed.png new file mode 100644 index 000000000..8cd395f3b Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_zl_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_zr.png b/src/android/app/src/main/res/drawable-hdpi/button_zr.png new file mode 100644 index 000000000..728fcf4d1 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_zr.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/button_zr_pressed.png b/src/android/app/src/main/res/drawable-hdpi/button_zr_pressed.png new file mode 100644 index 000000000..121877610 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/button_zr_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/dpad.png b/src/android/app/src/main/res/drawable-hdpi/dpad.png new file mode 100644 index 000000000..921b3902d Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/dpad.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_one_direction.png new file mode 100644 index 000000000..a8ffbb48a Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_one_direction.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_two_directions.png new file mode 100644 index 000000000..ceb994a6d Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/dpad_pressed_two_directions.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-hdpi/ic_cia_install.png new file mode 100644 index 000000000..8c00d8c34 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/ic_cia_install.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_folder.png b/src/android/app/src/main/res/drawable-hdpi/ic_folder.png new file mode 100644 index 000000000..90085252b Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/ic_folder.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_premium.png b/src/android/app/src/main/res/drawable-hdpi/ic_premium.png new file mode 100644 index 000000000..7dd45a405 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/ic_premium.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-hdpi/ic_settings_core.png new file mode 100644 index 000000000..2e7837020 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/ic_settings_core.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png new file mode 100644 index 000000000..2282f1a3b Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/ic_stat_notification_logo.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_c.png b/src/android/app/src/main/res/drawable-hdpi/stick_c.png new file mode 100644 index 000000000..d4c1d6c97 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/stick_c.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_c_pressed.png b/src/android/app/src/main/res/drawable-hdpi/stick_c_pressed.png new file mode 100644 index 000000000..c8d14c029 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/stick_c_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_c_range.png b/src/android/app/src/main/res/drawable-hdpi/stick_c_range.png new file mode 100644 index 000000000..8263d4b8d Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/stick_c_range.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_main.png b/src/android/app/src/main/res/drawable-hdpi/stick_main.png new file mode 100644 index 000000000..ae6d025a5 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/stick_main.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_main_pressed.png b/src/android/app/src/main/res/drawable-hdpi/stick_main_pressed.png new file mode 100644 index 000000000..ca469c6a7 Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/stick_main_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-hdpi/stick_main_range.png b/src/android/app/src/main/res/drawable-hdpi/stick_main_range.png new file mode 100644 index 000000000..9b5445edc Binary files /dev/null and b/src/android/app/src/main/res/drawable-hdpi/stick_main_range.png differ diff --git a/src/android/app/src/main/res/drawable-mdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-mdpi/ic_cia_install.png new file mode 100644 index 000000000..c6dc232b4 Binary files /dev/null and b/src/android/app/src/main/res/drawable-mdpi/ic_cia_install.png differ diff --git a/src/android/app/src/main/res/drawable-mdpi/ic_folder.png b/src/android/app/src/main/res/drawable-mdpi/ic_folder.png new file mode 100644 index 000000000..1e428dfe3 Binary files /dev/null and b/src/android/app/src/main/res/drawable-mdpi/ic_folder.png differ diff --git a/src/android/app/src/main/res/drawable-mdpi/ic_premium.png b/src/android/app/src/main/res/drawable-mdpi/ic_premium.png new file mode 100644 index 000000000..4dfb62596 Binary files /dev/null and b/src/android/app/src/main/res/drawable-mdpi/ic_premium.png differ diff --git a/src/android/app/src/main/res/drawable-night-hdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-night-hdpi/ic_cia_install.png new file mode 100644 index 000000000..cc986c8ac Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-hdpi/ic_cia_install.png differ diff --git a/src/android/app/src/main/res/drawable-night-hdpi/ic_folder.png b/src/android/app/src/main/res/drawable-night-hdpi/ic_folder.png new file mode 100644 index 000000000..ee688b09f Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-hdpi/ic_folder.png differ diff --git a/src/android/app/src/main/res/drawable-night-hdpi/ic_premium.png b/src/android/app/src/main/res/drawable-night-hdpi/ic_premium.png new file mode 100644 index 000000000..6b678d22c Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-hdpi/ic_premium.png differ diff --git a/src/android/app/src/main/res/drawable-night-hdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-night-hdpi/ic_settings_core.png new file mode 100644 index 000000000..bc9dc0beb Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-hdpi/ic_settings_core.png differ diff --git a/src/android/app/src/main/res/drawable-night-mdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-night-mdpi/ic_cia_install.png new file mode 100644 index 000000000..f61d84961 Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-mdpi/ic_cia_install.png differ diff --git a/src/android/app/src/main/res/drawable-night-mdpi/ic_folder.png b/src/android/app/src/main/res/drawable-night-mdpi/ic_folder.png new file mode 100644 index 000000000..05847c34b Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-mdpi/ic_folder.png differ diff --git a/src/android/app/src/main/res/drawable-night-mdpi/ic_premium.png b/src/android/app/src/main/res/drawable-night-mdpi/ic_premium.png new file mode 100644 index 000000000..87bac27df Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-mdpi/ic_premium.png differ diff --git a/src/android/app/src/main/res/drawable-night-xhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-night-xhdpi/ic_cia_install.png new file mode 100644 index 000000000..1eccbe68d Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xhdpi/ic_cia_install.png differ diff --git a/src/android/app/src/main/res/drawable-night-xhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-night-xhdpi/ic_folder.png new file mode 100644 index 000000000..ffa1d200e Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xhdpi/ic_folder.png differ diff --git a/src/android/app/src/main/res/drawable-night-xhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-night-xhdpi/ic_premium.png new file mode 100644 index 000000000..23a5cec51 Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xhdpi/ic_premium.png differ diff --git a/src/android/app/src/main/res/drawable-night-xhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-night-xhdpi/ic_settings_core.png new file mode 100644 index 000000000..9ca7975bb Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xhdpi/ic_settings_core.png differ diff --git a/src/android/app/src/main/res/drawable-night-xxhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_cia_install.png new file mode 100644 index 000000000..fc3c434b0 Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_cia_install.png differ diff --git a/src/android/app/src/main/res/drawable-night-xxhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_folder.png new file mode 100644 index 000000000..013600d1f Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_folder.png differ diff --git a/src/android/app/src/main/res/drawable-night-xxhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_premium.png new file mode 100644 index 000000000..2a0f1568f Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_premium.png differ diff --git a/src/android/app/src/main/res/drawable-night-xxhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_settings_core.png new file mode 100644 index 000000000..23706188b Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xxhdpi/ic_settings_core.png differ diff --git a/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_cia_install.png new file mode 100644 index 000000000..b4d1b92b7 Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_cia_install.png differ diff --git a/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_folder.png new file mode 100644 index 000000000..166bd052d Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_folder.png differ diff --git a/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_premium.png new file mode 100644 index 000000000..8d357b228 Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_premium.png differ diff --git a/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_settings_core.png new file mode 100644 index 000000000..32eb4faff Binary files /dev/null and b/src/android/app/src/main/res/drawable-night-xxxhdpi/ic_settings_core.png differ diff --git a/src/android/app/src/main/res/drawable-night/no_icon.png b/src/android/app/src/main/res/drawable-night/no_icon.png new file mode 100644 index 000000000..9a3969709 Binary files /dev/null and b/src/android/app/src/main/res/drawable-night/no_icon.png differ diff --git a/src/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index c7bd21dbd..000000000 --- a/src/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:aapt="http://schemas.android.com/aapt" - android:width="108dp" - android:height="108dp" - android:viewportHeight="108" - android:viewportWidth="108"> - <path - android:fillType="evenOdd" - android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z" - android:strokeColor="#00000000" - android:strokeWidth="1"> - <aapt:attr name="android:fillColor"> - <gradient - android:endX="78.5885" - android:endY="90.9159" - android:startX="48.7653" - android:startY="61.0927" - android:type="linear"> - <item - android:color="#44000000" - android:offset="0.0" /> - <item - android:color="#00000000" - android:offset="1.0" /> - </gradient> - </aapt:attr> - </path> - <path - android:fillColor="#FFFFFF" - android:fillType="nonZero" - android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z" - android:strokeColor="#00000000" - android:strokeWidth="1" /> -</vector> diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_a.png b/src/android/app/src/main/res/drawable-xhdpi/button_a.png new file mode 100644 index 000000000..4e20f2b0e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_a.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_a_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_a_pressed.png new file mode 100644 index 000000000..f18edd07e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_a_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_b.png b/src/android/app/src/main/res/drawable-xhdpi/button_b.png new file mode 100644 index 000000000..deb83a09d Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_b.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_b_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_b_pressed.png new file mode 100644 index 000000000..f583be028 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_b_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_l.png b/src/android/app/src/main/res/drawable-xhdpi/button_l.png new file mode 100644 index 000000000..d24039fbf Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_l.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_l_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_l_pressed.png new file mode 100644 index 000000000..378ac8751 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_l_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_r.png b/src/android/app/src/main/res/drawable-xhdpi/button_r.png new file mode 100644 index 000000000..7b01c043e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_r.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_r_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_r_pressed.png new file mode 100644 index 000000000..9b3e3e75a Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_r_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_select.png b/src/android/app/src/main/res/drawable-xhdpi/button_select.png new file mode 100644 index 000000000..57abf5666 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_select.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_select_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_select_pressed.png new file mode 100644 index 000000000..29eda72af Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_select_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_start.png b/src/android/app/src/main/res/drawable-xhdpi/button_start.png new file mode 100644 index 000000000..f9cf0d667 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_start.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_start_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_start_pressed.png new file mode 100644 index 000000000..4d690fa7e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_start_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_x.png b/src/android/app/src/main/res/drawable-xhdpi/button_x.png new file mode 100644 index 000000000..93a2ee997 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_x.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_x_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_x_pressed.png new file mode 100644 index 000000000..6bbd39646 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_x_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_y.png b/src/android/app/src/main/res/drawable-xhdpi/button_y.png new file mode 100644 index 000000000..d979e98e0 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_y.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_y_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_y_pressed.png new file mode 100644 index 000000000..a6c9bdb54 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_y_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_zl.png b/src/android/app/src/main/res/drawable-xhdpi/button_zl.png new file mode 100644 index 000000000..f94474fea Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_zl.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_zl_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_zl_pressed.png new file mode 100644 index 000000000..8f7d5ab7a Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_zl_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_zr.png b/src/android/app/src/main/res/drawable-xhdpi/button_zr.png new file mode 100644 index 000000000..a76658351 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_zr.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/button_zr_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/button_zr_pressed.png new file mode 100644 index 000000000..bbe4e64ce Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/button_zr_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/dpad.png b/src/android/app/src/main/res/drawable-xhdpi/dpad.png new file mode 100644 index 000000000..94ae84405 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/dpad.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_one_direction.png new file mode 100644 index 000000000..d6ccb2c4f Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_one_direction.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_two_directions.png new file mode 100644 index 000000000..2bba7749e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/dpad_pressed_two_directions.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-xhdpi/ic_cia_install.png new file mode 100644 index 000000000..839869401 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/ic_cia_install.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-xhdpi/ic_folder.png new file mode 100644 index 000000000..02bc3d75a Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/ic_folder.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-xhdpi/ic_premium.png new file mode 100644 index 000000000..ac4b19ff4 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/ic_premium.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-xhdpi/ic_settings_core.png new file mode 100644 index 000000000..8cff45f84 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/ic_settings_core.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png new file mode 100644 index 000000000..5e2787ba3 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/ic_stat_notification_logo.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_c.png b/src/android/app/src/main/res/drawable-xhdpi/stick_c.png new file mode 100644 index 000000000..7819f220a Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/stick_c.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_c_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/stick_c_pressed.png new file mode 100644 index 000000000..a111c2ac7 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/stick_c_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_c_range.png b/src/android/app/src/main/res/drawable-xhdpi/stick_c_range.png new file mode 100644 index 000000000..774c54292 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/stick_c_range.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_main.png b/src/android/app/src/main/res/drawable-xhdpi/stick_main.png new file mode 100644 index 000000000..3f80cdf6c Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/stick_main.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_main_pressed.png b/src/android/app/src/main/res/drawable-xhdpi/stick_main_pressed.png new file mode 100644 index 000000000..2a7675ef7 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/stick_main_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xhdpi/stick_main_range.png b/src/android/app/src/main/res/drawable-xhdpi/stick_main_range.png new file mode 100644 index 000000000..ca1672caf Binary files /dev/null and b/src/android/app/src/main/res/drawable-xhdpi/stick_main_range.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_a.png b/src/android/app/src/main/res/drawable-xxhdpi/button_a.png new file mode 100644 index 000000000..999b4c01e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_a.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_a_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_a_pressed.png new file mode 100644 index 000000000..bb4de9bd9 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_a_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_b.png b/src/android/app/src/main/res/drawable-xxhdpi/button_b.png new file mode 100644 index 000000000..8ed042e7e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_b.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_b_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_b_pressed.png new file mode 100644 index 000000000..86f5d535e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_b_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_l.png b/src/android/app/src/main/res/drawable-xxhdpi/button_l.png new file mode 100644 index 000000000..9572c66f8 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_l.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_l_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_l_pressed.png new file mode 100644 index 000000000..64bedc326 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_l_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_r.png b/src/android/app/src/main/res/drawable-xxhdpi/button_r.png new file mode 100644 index 000000000..abbcadede Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_r.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_r_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_r_pressed.png new file mode 100644 index 000000000..07421767f Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_r_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_select.png b/src/android/app/src/main/res/drawable-xxhdpi/button_select.png new file mode 100644 index 000000000..42c3b7c43 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_select.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_select_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_select_pressed.png new file mode 100644 index 000000000..0d1e56f6a Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_select_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_start.png b/src/android/app/src/main/res/drawable-xxhdpi/button_start.png new file mode 100644 index 000000000..4e9585bb4 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_start.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_start_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_start_pressed.png new file mode 100644 index 000000000..8c089e237 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_start_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_x.png b/src/android/app/src/main/res/drawable-xxhdpi/button_x.png new file mode 100644 index 000000000..0500f964f Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_x.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_x_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_x_pressed.png new file mode 100644 index 000000000..56db5843d Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_x_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_y.png b/src/android/app/src/main/res/drawable-xxhdpi/button_y.png new file mode 100644 index 000000000..53c5ca084 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_y.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_y_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_y_pressed.png new file mode 100644 index 000000000..5d91cbfb0 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_y_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_zl.png b/src/android/app/src/main/res/drawable-xxhdpi/button_zl.png new file mode 100644 index 000000000..f8ce9a0c6 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_zl.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_zl_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_zl_pressed.png new file mode 100644 index 000000000..981c8b0c8 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_zl_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_zr.png b/src/android/app/src/main/res/drawable-xxhdpi/button_zr.png new file mode 100644 index 000000000..82065e126 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_zr.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/button_zr_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/button_zr_pressed.png new file mode 100644 index 000000000..b30b2e799 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/button_zr_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/dpad.png b/src/android/app/src/main/res/drawable-xxhdpi/dpad.png new file mode 100644 index 000000000..36b7ea183 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/dpad.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_one_direction.png new file mode 100644 index 000000000..3715e1c11 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_one_direction.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_two_directions.png new file mode 100644 index 000000000..fb0d7fc5c Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/dpad_pressed_two_directions.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_cia_install.png new file mode 100644 index 000000000..e6812f0d4 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/ic_cia_install.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_folder.png new file mode 100644 index 000000000..05f429614 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/ic_folder.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_premium.png new file mode 100644 index 000000000..63f162e52 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/ic_premium.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_settings_core.png new file mode 100644 index 000000000..0b9049f46 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/ic_settings_core.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png b/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png new file mode 100644 index 000000000..06cef9de3 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/ic_stat_notification_logo.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_c.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_c.png new file mode 100644 index 000000000..e950c5b15 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/stick_c.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_c_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_c_pressed.png new file mode 100644 index 000000000..3ac88ed9b Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/stick_c_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_c_range.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_c_range.png new file mode 100644 index 000000000..a3491c80f Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/stick_c_range.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_main.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_main.png new file mode 100644 index 000000000..16ca58c0f Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/stick_main.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_main_pressed.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_main_pressed.png new file mode 100644 index 000000000..e7fe0c2d5 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/stick_main_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxhdpi/stick_main_range.png b/src/android/app/src/main/res/drawable-xxhdpi/stick_main_range.png new file mode 100644 index 000000000..8c47b2ba3 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxhdpi/stick_main_range.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_a.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_a.png new file mode 100644 index 000000000..e364fae1e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_a.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_a_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_a_pressed.png new file mode 100644 index 000000000..08d65cc99 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_a_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_b.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_b.png new file mode 100644 index 000000000..faae9b6f7 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_b.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_b_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_b_pressed.png new file mode 100644 index 000000000..669780f28 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_b_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_l.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_l.png new file mode 100644 index 000000000..888b147de Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_l.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_l_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_l_pressed.png new file mode 100644 index 000000000..605493e3e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_l_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_r.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_r.png new file mode 100644 index 000000000..90a93af8d Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_r.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_r_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_r_pressed.png new file mode 100644 index 000000000..4500cd2be Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_r_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_select.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_select.png new file mode 100644 index 000000000..b18b2fd59 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_select.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_select_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_select_pressed.png new file mode 100644 index 000000000..53ed400e0 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_select_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_start.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_start.png new file mode 100644 index 000000000..c55e56852 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_start.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_start_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_start_pressed.png new file mode 100644 index 000000000..1507cc365 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_start_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_x.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_x.png new file mode 100644 index 000000000..7ef2b883e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_x.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_x_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_x_pressed.png new file mode 100644 index 000000000..f3f11ede2 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_x_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_y.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_y.png new file mode 100644 index 000000000..4ce679c69 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_y.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_y_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_y_pressed.png new file mode 100644 index 000000000..926f5e269 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_y_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_zl.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_zl.png new file mode 100644 index 000000000..7faf8db3b Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_zl.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_zl_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_zl_pressed.png new file mode 100644 index 000000000..cc56a749c Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_zl_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_zr.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_zr.png new file mode 100644 index 000000000..ed1b6b683 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_zr.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/button_zr_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/button_zr_pressed.png new file mode 100644 index 000000000..892fa74f1 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/button_zr_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/dpad.png b/src/android/app/src/main/res/drawable-xxxhdpi/dpad.png new file mode 100644 index 000000000..6272f39e6 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/dpad.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_one_direction.png b/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_one_direction.png new file mode 100644 index 000000000..0cccd3a30 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_one_direction.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_two_directions.png b/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_two_directions.png new file mode 100644 index 000000000..18a99ad2d Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/dpad_pressed_two_directions.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/ic_cia_install.png b/src/android/app/src/main/res/drawable-xxxhdpi/ic_cia_install.png new file mode 100644 index 000000000..69ae32dc3 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/ic_cia_install.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/ic_folder.png b/src/android/app/src/main/res/drawable-xxxhdpi/ic_folder.png new file mode 100644 index 000000000..c85074c60 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/ic_folder.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/ic_premium.png b/src/android/app/src/main/res/drawable-xxxhdpi/ic_premium.png new file mode 100644 index 000000000..6f1550a10 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/ic_premium.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/ic_settings_core.png b/src/android/app/src/main/res/drawable-xxxhdpi/ic_settings_core.png new file mode 100644 index 000000000..2827a1777 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/ic_settings_core.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_c.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c.png new file mode 100644 index 000000000..88e09b8a0 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_pressed.png new file mode 100644 index 000000000..edc920e8e Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_range.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_range.png new file mode 100644 index 000000000..a8b693494 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/stick_c_range.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_main.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main.png new file mode 100644 index 000000000..d157edca2 Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_pressed.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_pressed.png new file mode 100644 index 000000000..2ac2440be Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_pressed.png differ diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_range.png b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_range.png new file mode 100644 index 000000000..71e67e02a Binary files /dev/null and b/src/android/app/src/main/res/drawable-xxxhdpi/stick_main_range.png differ diff --git a/src/android/app/src/main/res/drawable/gamelist_divider.xml b/src/android/app/src/main/res/drawable/gamelist_divider.xml new file mode 100644 index 000000000..7da9dccce --- /dev/null +++ b/src/android/app/src/main/res/drawable/gamelist_divider.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + + <size + android:width="1dp" + android:height="1dp" /> + + <solid android:color="@color/gamelist_divider" /> + +</shape> diff --git a/src/android/app/src/main/res/drawable/ic_add.xml b/src/android/app/src/main/res/drawable/ic_add.xml deleted file mode 100644 index e3979cd7f..000000000 --- a/src/android/app/src/main/res/drawable/ic_add.xml +++ /dev/null @@ -1,5 +0,0 @@ -<vector android:height="24dp" android:tint="#FFFFFF" - android:viewportHeight="24.0" android:viewportWidth="24.0" - android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> - <path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> -</vector> diff --git a/src/android/app/src/main/res/drawable/ic_launcher_background.xml b/src/android/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index d5fccc538..000000000 --- a/src/android/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="108dp" - android:height="108dp" - android:viewportHeight="108" - android:viewportWidth="108"> - <path - android:fillColor="#26A69A" - android:pathData="M0,0h108v108h-108z" /> - <path - android:fillColor="#00000000" - android:pathData="M9,0L9,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,0L19,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M29,0L29,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M39,0L39,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M49,0L49,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M59,0L59,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M69,0L69,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M79,0L79,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M89,0L89,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M99,0L99,108" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,9L108,9" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,19L108,19" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,29L108,29" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,39L108,39" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,49L108,49" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,59L108,59" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,69L108,69" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,79L108,79" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,89L108,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M0,99L108,99" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,29L89,29" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,39L89,39" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,49L89,49" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,59L89,59" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,69L89,69" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M19,79L89,79" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M29,19L29,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M39,19L39,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M49,19L49,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M59,19L59,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M69,19L69,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> - <path - android:fillColor="#00000000" - android:pathData="M79,19L79,89" - android:strokeColor="#33FFFFFF" - android:strokeWidth="0.8" /> -</vector> diff --git a/src/android/app/src/main/res/drawable/no_icon.png b/src/android/app/src/main/res/drawable/no_icon.png new file mode 100644 index 000000000..1ce8fdc76 Binary files /dev/null and b/src/android/app/src/main/res/drawable/no_icon.png differ diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml new file mode 100644 index 000000000..7d7f36925 --- /dev/null +++ b/src/android/app/src/main/res/layout/activity_emulation.xml @@ -0,0 +1,17 @@ +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/frame_content" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <FrameLayout + android:id="@+id/frame_emulation_fragment" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <ImageView + android:id="@+id/image_icon" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:transitionName="image_game_icon" /> + +</FrameLayout> \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml index d13b8e03e..cea0922a7 100644 --- a/src/android/app/src/main/res/layout/activity_main.xml +++ b/src/android/app/src/main/res/layout/activity_main.xml @@ -1,50 +1,27 @@ <?xml version="1.0" encoding="utf-8"?> -<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/coordinator_main" - android:layout_width="match_parent" - android:layout_height="match_parent"> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/coordinator_main" + android:layout_width="match_parent" + android:layout_height="match_parent"> - <android.support.design.widget.AppBarLayout + <FrameLayout + android:id="@+id/games_platform_frame" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <com.google.android.material.appbar.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> + android:layout_height="wrap_content"> - <android.support.v7.widget.Toolbar + <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar_main" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - app:popupTheme="@style/ThemeOverlay.AppCompat.Light" - app:layout_scrollFlags="scroll|enterAlways"/> + android:background="?colorPrimary"/> - <android.support.design.widget.TabLayout - android:id="@+id/tabs_platforms" - android:layout_width="match_parent" - android:layout_height="wrap_content" - app:tabTextAppearance="@style/MyCustomTextAppearance" - app:tabMode="fixed" - app:tabGravity="fill"/> + </com.google.android.material.appbar.AppBarLayout> - </android.support.design.widget.AppBarLayout> - - <android.support.v4.view.ViewPager - android:id="@+id/pager_platforms" - android:layout_width="match_parent" - android:layout_height="match_parent" - app:layout_behavior="@string/appbar_scrolling_view_behavior"/> - - <android.support.design.widget.FloatingActionButton - android:id="@+id/button_add_directory" - style="@style/CitraBase" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_margin="16dp" - android:src="@drawable/ic_add" - app:backgroundTint="@color/citra_orange_dark" - app:borderWidth="0dp" - app:layout_anchor="@+id/pager_platforms" - app:layout_anchorGravity="bottom|right|end" - app:rippleColor="?android:colorPrimaryDark" /> - -</android.support.design.widget.CoordinatorLayout> +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 000000000..11b91c45f --- /dev/null +++ b/src/android/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/frame_content" /> diff --git a/src/android/app/src/main/res/layout/card_game.xml b/src/android/app/src/main/res/layout/card_game.xml new file mode 100644 index 000000000..6e87490f9 --- /dev/null +++ b/src/android/app/src/main/res/layout/card_game.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clickable="true" + android:focusable="true" + android:foreground="?android:attr/selectableItemBackground" + android:transitionName="card_game" + tools:layout_width="match_parent"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/linearLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="8dp"> + + <ImageView + android:id="@+id/image_game_screen" + android:layout_width="56dp" + android:layout_height="56dp" + android:adjustViewBounds="false" + android:cropToPadding="false" + android:scaleType="fitCenter" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:scaleType="fitCenter" /> + + <TextView + android:id="@+id/text_game_title" + style="@android:style/TextAppearance.Material.Subhead" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:baselineAligned="false" + android:ellipsize="end" + android:gravity="center_vertical" + android:lines="1" + android:maxLines="1" + android:textAlignment="viewStart" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/image_game_screen" + app:layout_constraintTop_toTopOf="parent" + tools:text="The Legend of Zelda\nOcarina of Time 3D" + android:textColor="@color/header_text" /> + + <TextView + android:id="@+id/text_company" + style="@android:style/TextAppearance.Material.Caption" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:lines="1" + android:maxLines="1" + app:layout_constraintBottom_toBottomOf="@+id/image_game_screen" + app:layout_constraintStart_toStartOf="@+id/text_game_title" + app:layout_constraintTop_toBottomOf="@+id/text_game_title" + app:layout_constraintVertical_bias="0.842" + tools:text="Nintendo" + android:textColor="@color/header_subtext" /> + + <TextView + android:id="@+id/text_filename" + style="@android:style/TextAppearance.Material.Caption" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:lines="1" + android:maxLines="1" + app:layout_constraintBottom_toBottomOf="@+id/image_game_screen" + app:layout_constraintStart_toStartOf="@+id/text_game_title" + app:layout_constraintTop_toBottomOf="@+id/text_game_title" + app:layout_constraintVertical_bias="0.0" + tools:text="Pilotwings_Resort.cxi" + android:textColor="@color/header_subtext" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</androidx.cardview.widget.CardView> + diff --git a/src/android/app/src/main/res/layout/dialog_checkbox.xml b/src/android/app/src/main/res/layout/dialog_checkbox.xml new file mode 100644 index 000000000..c0f307117 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_checkbox.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:paddingTop="5dp" + android:paddingLeft="20dp" + android:paddingRight="20dp" + android:paddingBottom="0dp" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <CheckBox + android:id="@+id/checkBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/do_not_show_this_again" /> +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/dialog_progress_bar.xml b/src/android/app/src/main/res/layout/dialog_progress_bar.xml new file mode 100644 index 000000000..a81157a29 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_progress_bar.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <ProgressBar + android:id="@+id/progress_bar" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/spacing_large" + android:layout_marginRight="@dimen/spacing_large" + android:layout_alignParentEnd="true" + android:layout_below="@+id/progress_text" + android:layout_alignParentStart="true"/> + + <TextView + android:id="@+id/progress_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/spacing_large" + android:layout_marginRight="@dimen/spacing_large" + android:gravity="right" + android:text="1/100" /> +</LinearLayout> \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/dialog_seekbar.xml b/src/android/app/src/main/res/layout/dialog_seekbar.xml new file mode 100644 index 000000000..35abecfcb --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_seekbar.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <SeekBar + android:id="@+id/seekbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/spacing_large" + android:layout_marginRight="@dimen/spacing_large" + android:layout_alignParentEnd="true" + android:layout_alignParentStart="true" + android:layout_below="@+id/text_value" + android:layout_marginBottom="@dimen/spacing_medlarge" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:text="75" + android:id="@+id/text_value" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:layout_marginTop="@dimen/spacing_medlarge" + android:layout_marginBottom="@dimen/spacing_medlarge" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:text="%" + android:id="@+id/text_units" + android:layout_alignTop="@+id/text_value" + android:layout_toEndOf="@+id/text_value" /> + +</RelativeLayout> \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/filepicker_toolbar.xml b/src/android/app/src/main/res/layout/filepicker_toolbar.xml new file mode 100644 index 000000000..644934171 --- /dev/null +++ b/src/android/app/src/main/res/layout/filepicker_toolbar.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/nnf_picker_toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:background="?attr/colorPrimary" + android:minHeight="?attr/actionBarSize" + android:theme="?nnf_toolbarTheme"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/filepicker_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="start" + android:singleLine="true" + android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" /> + + <TextView + android:id="@+id/nnf_current_dir" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="start" + android:singleLine="true" + android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle" /> + </LinearLayout> +</androidx.appcompat.widget.Toolbar> diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml new file mode 100644 index 000000000..d6e47e1e4 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_emulation.xml @@ -0,0 +1,47 @@ +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:keepScreenOn="true" + tools:context="org.citra.citra_emu.fragments.EmulationFragment"> + + <!-- This is what everything is rendered to during emulation --> + <SurfaceView + android:id="@+id/surface_emulation" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:focusable="false" + android:focusableInTouchMode="false" /> + + <!-- This is the onscreen input overlay --> + <org.citra.citra_emu.overlay.InputOverlay + android:id="@+id/surface_input_overlay" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:focusable="true" + android:focusableInTouchMode="true" /> + + <TextView + android:id="@+id/show_fps_text" + android:layout_marginStart="8dp" + android:layout_marginTop="2dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clickable="false" + android:linksClickable="false" + android:longClickable="false" + android:shadowColor="@android:color/black" + android:textColor="@android:color/white" + android:textSize="12sp" /> + + <Button + android:id="@+id/done_control_config" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:padding="@dimen/spacing_small" + android:background="@color/citra_orange" + android:text="@string/emulation_done" + android:visibility="gone" /> + +</FrameLayout> diff --git a/src/android/app/src/main/res/layout/fragment_grid.xml b/src/android/app/src/main/res/layout/fragment_grid.xml new file mode 100644 index 000000000..f5b6c2e19 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_grid.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.swiperefreshlayout.widget.SwipeRefreshLayout + android:id="@+id/refresh_grid_games" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/gamelist_empty_text" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:text="@string/empty_gamelist" + android:visibility="gone" + android:textSize="18sp" + android:gravity="center" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/grid_games" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:listitem="@layout/card_game" /> + </RelativeLayout> + + </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> +</FrameLayout> \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/fragment_settings.xml b/src/android/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 000000000..4c5d597c1 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<org.citra.citra_emu.features.settings.ui.SettingsFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/list_settings" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/card_view_background" /> + +</org.citra.citra_emu.features.settings.ui.SettingsFrameLayout> \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/list_item_setting.xml b/src/android/app/src/main/res/layout/list_item_setting.xml new file mode 100644 index 000000000..df83684f7 --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_setting.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:gravity="center_vertical" + android:minHeight="72dp" + android:paddingTop="@dimen/spacing_large" + android:paddingBottom="@dimen/spacing_large"> + + <TextView + android:id="@+id/text_setting_name" + style="@style/TextAppearance.AppCompat.Headline" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:layout_alignParentEnd="true" + android:layout_marginStart="@dimen/spacing_large" + android:layout_marginEnd="@dimen/spacing_large" + android:textColor="@color/header_text" + android:textSize="16sp" + tools:text="Setting Name" /> + + <TextView + android:id="@+id/text_setting_description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/text_setting_name" + android:layout_alignStart="@+id/text_setting_name" + android:layout_alignParentStart="true" + android:layout_alignParentEnd="true" + android:layout_marginStart="@dimen/spacing_large" + android:layout_marginTop="@dimen/spacing_small" + android:layout_marginEnd="@dimen/spacing_large" + android:visibility="visible" + tools:text="@string/app_disclaimer" + android:textColor="@color/header_subtext" /> + +</RelativeLayout> \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/list_item_setting_checkbox.xml b/src/android/app/src/main/res/layout/list_item_setting_checkbox.xml new file mode 100644 index 000000000..86ba83f11 --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_setting_checkbox.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="72dp" + android:background="?android:attr/selectableItemBackground" + android:focusable="true" + android:clickable="true"> + + <TextView + android:id="@+id/text_setting_name" + style="@style/TextAppearance.AppCompat.Headline" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:layout_marginEnd="@dimen/spacing_large" + android:layout_marginStart="@dimen/spacing_large" + android:layout_marginTop="@dimen/spacing_large" + android:layout_toStartOf="@+id/checkbox" + android:textColor="@color/header_text" + android:textSize="16sp" + tools:text="@string/frame_limit_enable" /> + + <TextView + android:id="@+id/text_setting_description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_alignStart="@+id/text_setting_name" + android:layout_below="@+id/text_setting_name" + android:layout_marginBottom="@dimen/spacing_large" + android:layout_marginEnd="@dimen/spacing_large" + android:layout_marginStart="@dimen/spacing_large" + android:layout_marginTop="@dimen/spacing_small" + android:layout_toStartOf="@+id/checkbox" + android:textAlignment="textStart" + android:textColor="@color/header_subtext" + tools:text="@string/frame_limit_enable_description" /> + + <CheckBox + android:id="@+id/checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:layout_marginEnd="@dimen/spacing_large" + android:focusable="false" + android:clickable="false" /> + +</RelativeLayout> \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/list_item_settings_header.xml b/src/android/app/src/main/res/layout/list_item_settings_header.xml new file mode 100644 index 000000000..d220dfd61 --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_settings_header.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="48dp"> + + <TextView + android:id="@+id/text_header_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_marginStart="@dimen/spacing_large" + android:layout_marginBottom="@dimen/spacing_small" + android:layout_marginTop="@dimen/spacing_small" + android:textColor="?android:colorAccent" + android:textStyle="bold" + tools:text="CPU Settings" /> + +</FrameLayout> \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/premium_item_setting.xml b/src/android/app/src/main/res/layout/premium_item_setting.xml new file mode 100644 index 000000000..17d5a13b2 --- /dev/null +++ b/src/android/app/src/main/res/layout/premium_item_setting.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:gravity="center_vertical" + android:minHeight="72dp" + android:paddingTop="@dimen/spacing_large" + android:paddingBottom="@dimen/spacing_large"> + + <TextView + android:id="@+id/text_setting_name" + style="@style/TextAppearance.AppCompat.Headline" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:layout_alignParentEnd="true" + android:layout_marginStart="@dimen/spacing_large" + android:layout_marginEnd="@dimen/spacing_large" + android:textColor="?android:colorAccent" + android:textStyle="bold" + tools:text="Setting Name" /> + + <TextView + android:id="@+id/text_setting_description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/text_setting_name" + android:layout_alignStart="@+id/text_setting_name" + android:layout_alignParentStart="true" + android:layout_alignParentEnd="true" + android:layout_marginStart="@dimen/spacing_large" + android:layout_marginTop="@dimen/spacing_small" + android:layout_marginEnd="@dimen/spacing_large" + android:visibility="visible" + tools:text="@string/app_disclaimer" + android:textColor="@color/header_subtext" /> + +</RelativeLayout> \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml b/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml new file mode 100644 index 000000000..d082f5283 --- /dev/null +++ b/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="8dp" + android:gravity="center"> + + <DatePicker + android:id="@+id/date_picker" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:calendarViewShown="false" + android:datePickerMode="spinner" + android:spinnersShown="true" /> + + <TimePicker + android:id="@+id/time_picker" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:timePickerMode="spinner" /> +</LinearLayout> diff --git a/src/android/app/src/main/res/menu/menu_emulation.xml b/src/android/app/src/main/res/menu/menu_emulation.xml new file mode 100644 index 000000000..ea3301d37 --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_emulation.xml @@ -0,0 +1,113 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + tools:context="org.citra.citra_emu.activities.EmulationActivity"> + + <item + android:id="@+id/menu_emulation_save_state" + android:title="@string/emulation_save_state"> + <menu/> + </item> + + <item + android:id="@+id/menu_emulation_load_state" + android:title="@string/emulation_load_state"> + <menu/> + </item> + + <item + android:id="@+id/menu_emulation_configure_controls" + android:title="@string/emulation_configure_controls"> + <menu> + <item + android:id="@+id/menu_emulation_edit_layout" + android:title="@string/emulation_edit_layout" /> + + <item + android:id="@+id/menu_emulation_toggle_controls" + android:title="@string/emulation_toggle_controls" /> + + <item + android:id="@+id/menu_emulation_adjust_scale" + android:title="@string/emulation_control_scale" /> + + <group android:checkableBehavior="all"> + <item + android:id="@+id/menu_emulation_joystick_rel_center" + android:checkable="true" + android:title="@string/emulation_control_joystick_rel_center"/> + <item + android:id="@+id/menu_emulation_dpad_slide_enable" + android:checkable="true" + android:title="@string/emulation_control_dpad_slide_enable" /> + </group> + + <item + android:id="@+id/menu_emulation_reset_overlay" + android:title="@string/emulation_touch_overlay_reset" /> + </menu> + </item> + + <item + android:id="@+id/menu_emulation_amiibo" + android:title="@string/menu_emulation_amiibo"> + <menu> + <item + android:id="@+id/menu_emulation_amiibo_load" + android:title="@string/menu_emulation_amiibo_load" /> + + <item + android:id="@+id/menu_emulation_amiibo_remove" + android:title="@string/menu_emulation_amiibo_remove" /> + </menu> + </item> + + <item + android:id="@+id/menu_emulation_switch_screen_layout" + app:showAsAction="never" + android:title="@string/emulation_switch_screen_layout"> + <menu> + <group android:checkableBehavior="single"> + <item + android:id="@+id/menu_screen_layout_landscape" + android:title="@string/emulation_screen_layout_landscape" /> + + <item + android:id="@+id/menu_screen_layout_portrait" + android:title="@string/emulation_screen_layout_portrait" /> + + <item + android:id="@+id/menu_screen_layout_single" + android:title="@string/emulation_screen_layout_single" /> + + <item + android:id="@+id/menu_screen_layout_sidebyside" + android:title="@string/emulation_screen_layout_sidebyside" /> + </group> + </menu> + </item> + + <item + android:id="@+id/menu_emulation_swap_screens" + app:showAsAction="never" + android:title="@string/emulation_swap_screens" + android:checkable="true" /> + + <item + android:id="@+id/menu_emulation_show_fps" + app:showAsAction="never" + android:title="@string/emulation_show_fps" + android:checkable="true" /> + + <item + android:id="@+id/menu_emulation_show_overlay" + app:showAsAction="never" + android:title="@string/emulation_show_overlay" + android:checkable="true" /> + + <item + android:id="@+id/menu_emulation_open_settings" + app:showAsAction="never" + android:title="@string/emulation_open_settings" /> + +</menu> diff --git a/src/android/app/src/main/res/menu/menu_game_grid.xml b/src/android/app/src/main/res/menu/menu_game_grid.xml new file mode 100644 index 000000000..9cdcc7f08 --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_game_grid.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/button_premium" + android:icon="@drawable/ic_premium" + android:title="@string/premium_text" + app:showAsAction="ifRoom" /> + <item + android:id="@+id/button_file_menu" + android:icon="@drawable/ic_folder" + android:title="@string/select_game_folder" + app:showAsAction="ifRoom"> + <menu> + <item + android:id="@+id/button_add_directory" + android:icon="@drawable/ic_folder" + android:title="@string/select_game_folder" + app:showAsAction="ifRoom" /> + <item + android:id="@+id/button_install_cia" + android:icon="@drawable/ic_cia_install" + android:title="@string/install_cia_title" + app:showAsAction="ifRoom" /> + </menu> + </item> + <item + android:id="@+id/menu_settings_core" + android:icon="@drawable/ic_settings_core" + android:title="@string/grid_menu_core_settings" + app:showAsAction="ifRoom" /> + +</menu> diff --git a/src/android/app/src/main/res/menu/menu_settings.xml b/src/android/app/src/main/res/menu/menu_settings.xml new file mode 100644 index 000000000..1fe7aa6d4 --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_settings.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu /> \ No newline at end of file diff --git a/src/android/app/src/main/res/mipmap-anydpi-v26/ic_citra_round.xml b/src/android/app/src/main/res/mipmap-anydpi-v26/ic_citra_round.xml deleted file mode 100644 index 036d09bc5..000000000 --- a/src/android/app/src/main/res/mipmap-anydpi-v26/ic_citra_round.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@color/ic_launcher_background"/> - <foreground android:drawable="@mipmap/ic_launcher_foreground"/> -</adaptive-icon> \ No newline at end of file diff --git a/src/android/app/src/main/res/mipmap-anydpi-v26/ic_citra.xml b/src/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 96% rename from src/android/app/src/main/res/mipmap-anydpi-v26/ic_citra.xml rename to src/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 036d09bc5..c9ad5f98f 100644 --- a/src/android/app/src/main/res/mipmap-anydpi-v26/ic_citra.xml +++ b/src/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@color/ic_launcher_background"/> - <foreground android:drawable="@mipmap/ic_launcher_foreground"/> + <background android:drawable="@color/ic_launcher_background" /> + <foreground android:drawable="@mipmap/ic_launcher_foreground" /> </adaptive-icon> \ No newline at end of file diff --git a/src/android/app/src/main/res/mipmap-hdpi/ic_citra.png b/src/android/app/src/main/res/mipmap-hdpi/ic_citra.png deleted file mode 100644 index 23fb939e0..000000000 Binary files a/src/android/app/src/main/res/mipmap-hdpi/ic_citra.png and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-hdpi/ic_citra_round.png b/src/android/app/src/main/res/mipmap-hdpi/ic_citra_round.png deleted file mode 100644 index f1ff8f1e4..000000000 Binary files a/src/android/app/src/main/res/mipmap-hdpi/ic_citra_round.png and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/src/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..57ea32d88 Binary files /dev/null and b/src/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/src/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index 6b23d66f1..18cc694d1 100644 Binary files a/src/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/src/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src/android/app/src/main/res/mipmap-mdpi/ic_citra.png b/src/android/app/src/main/res/mipmap-mdpi/ic_citra.png deleted file mode 100644 index 1b50f37ff..000000000 Binary files a/src/android/app/src/main/res/mipmap-mdpi/ic_citra.png and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-mdpi/ic_citra_round.png b/src/android/app/src/main/res/mipmap-mdpi/ic_citra_round.png deleted file mode 100644 index c21c0cab2..000000000 Binary files a/src/android/app/src/main/res/mipmap-mdpi/ic_citra_round.png and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/src/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..7052f4077 Binary files /dev/null and b/src/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/src/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index d577ec583..0e7cdeed6 100644 Binary files a/src/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/src/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src/android/app/src/main/res/mipmap-xhdpi/ic_citra.png b/src/android/app/src/main/res/mipmap-xhdpi/ic_citra.png deleted file mode 100644 index a39b72567..000000000 Binary files a/src/android/app/src/main/res/mipmap-xhdpi/ic_citra.png and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-xhdpi/ic_citra_round.png b/src/android/app/src/main/res/mipmap-xhdpi/ic_citra_round.png deleted file mode 100644 index fc3faff81..000000000 Binary files a/src/android/app/src/main/res/mipmap-xhdpi/ic_citra_round.png and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..4d3e9fc41 Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index c2deb815c..b57a8d623 100644 Binary files a/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/src/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src/android/app/src/main/res/mipmap-xxhdpi/ic_citra.png b/src/android/app/src/main/res/mipmap-xxhdpi/ic_citra.png deleted file mode 100644 index 8d4c19ba0..000000000 Binary files a/src/android/app/src/main/res/mipmap-xxhdpi/ic_citra.png and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-xxhdpi/ic_citra_round.png b/src/android/app/src/main/res/mipmap-xxhdpi/ic_citra_round.png deleted file mode 100644 index fac2dcfe6..000000000 Binary files a/src/android/app/src/main/res/mipmap-xxhdpi/ic_citra_round.png and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d2c6d0692 Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index 2e3b41ede..22f6eb36f 100644 Binary files a/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/src/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_citra.png b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_citra.png deleted file mode 100644 index a8d3281af..000000000 Binary files a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_citra.png and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_citra_round.png b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_citra_round.png deleted file mode 100644 index 09aa0ca98..000000000 Binary files a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_citra_round.png and /dev/null differ diff --git a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..1aa7f3ae2 Binary files /dev/null and b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index b2372bef7..b57c8f75b 100644 Binary files a/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/src/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..403e94894 --- /dev/null +++ b/src/android/app/src/main/res/values-de/strings.xml @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_disclaimer">Auf dieser Software laufen Spiele für die Nintendo 3DS Handheld-Spielekonsole. Keine Spiele sind enthalten.\n\nVor dem Ausführen, legen Sie bitte ihre rechtmäßig erworbenen 3DS-Spieldateien auf dem Gerätespeicher ab.</string> + <string name="app_notification_channel_description">Citra 3DS Emulator Benachrichtigungen</string> + <string name="app_notification_running">Citra läuft</string> + + <!-- Input related strings --> + <string name="controller_circlepad">Schiebepad</string> + <string name="controller_c">C-Stick</string> + <string name="controller_triggers">Schultertasten</string> + <string name="controller_dpad">Steuerkreuz</string> + <string name="controller_axis_vertical">Hoch/Runter Achse</string> + <string name="controller_axis_horizontal">Links/Rechts Achse</string> + <string name="input_binding">Eingabebindung</string> + <string name="input_binding_description">Machen sie eine Eingabe, um sie an %1$s zu binden.</string> + <string name="input_binding_description_vertical_axis">Bewegen sie den Joystick nach oben oder unten.</string> + <string name="input_binding_description_horizontal_axis">Bewegen sie den Joystick nach links oder rechts.</string> + <string name="input_message_analog_only">Diese Eingabe muss an einen Gamepad-Analogstick oder das Steuerkreuz gebunden sein!</string> + <string name="input_message_button_only">Diese Eingabe muss an einen Gamepad-Knopf gebunden sein!</string> + + <!-- Generic buttons (Shared with lots of stuff) --> + <string name="generic_buttons">Knöpfe</string> + + <!-- Premium settings strings --> + <string name="design">Theme ändern (Hell, Dunkel)</string> + <string name="design_updated">Theme wird beim Verlassen der Einstellungen aktualisiert</string> + + <!-- Core settings strings --> + <string name="cpu_jit">CPU JIT aktivieren</string> + <string name="cpu_jit_description">Benutzt den Just-in-Time (JIT) Compiler für die CPU-Emulation. Wenn aktiviert wird die Spieleleistung stark verbessert.</string> + <string name="init_clock">Systemuhrtyp</string> + <string name="init_clock_description">Stellen Sie die emulierte 3DS-Uhr so ein, dass sie entweder die Ihres Geräts widerspiegelt oder zu einem simulierten Datum und einer simulierten Uhrzeit startet.</string> + + <!-- System settings strings --> + <string name="init_time">Überschriebene Startzeit der Systemuhr</string> + <string name="init_time_description">Falls der \"Systemuhrtyp\" auf \"Simulierte Uhr\" gesetzt ist, ändert dies das Startdatum und die Startzeit.</string> + <string name="emulated_region">Emulierte Region</string> + <string name="emulated_language">Emulierte Sprache</string> + + <!-- Camera settings strings --> + <string name="inner_camera">Innenkamera</string> + <string name="outer_left_camera">Außenkamera Links</string> + <string name="outer_right_camera">Außenkamera Rechts</string> + <string name="image_source">Bildquelle</string> + <string name="image_source_description">Legt die Bildquelle der virtuellen Kamera fest. Sie können eine Bilddatei oder, falls unterstützt, die Kamera das Geräts verwenden.</string> + <string name="camera_device">Kameragerät</string> + <string name="camera_device_description">Wenn die Einstellung \"Bildquelle\" auf \"Gerätekamera\" eingestellt ist, wird hiermit die zu verwendende Kamera eingestellt.</string> + <string name="camera_facing_front">Vorderseite</string> + <string name="camera_facing_back">Rückseite</string> + <string name="camera_facing_external">Extern</string> + <string name="image_flip">Bilddrehung</string> + + <!-- Graphics settings strings --> + <string name="vsync">V-Sync aktivieren</string> + <string name="vsync_description">Synchronisiert die Bildrate des Spiels mit der Bildwiederholrate des Geräts.</string> + <string name="linear_filtering">Lineare Filterung aktivieren</string> + <string name="linear_filtering_description">Aktiviert lineare Filterung, welche die Spieletexturen glättet.</string> + <string name="texture_filter_name">Texturfilter</string> + <string name="texture_filter_description">Verbessert die Grafik von Spielen durch das Anwendung eines Texturfilters. Die unterstützten Filter sind Anime4K Ultrafast, Bicubic, ScaleForce und xBRZ freescale.</string> + <string name="hw_renderer">Aktiviere Hardware Renderer</string> + <string name="hw_renderer_description">Benutzt Hardware, um die 3DS-Grafik zu emulieren. Wenn aktiviert, wird die Spieleleistung stark verbessert.</string> + <string name="hw_shaders">Aktiviere Hardware Shader</string> + <string name="hw_shaders_description">Benutzt Hardware, um die 3DS-Shader zu emulieren. Wenn aktiviert, wird die Spieleleistung stark verbessert.</string> + <string name="shaders_accurate_mul">Aktiviere genaue Shader-Multiplikation</string> + <string name="shaders_accurate_mul_description">Benutzt genauere Multiplikation in Hardware-Shadern, welche einige Grafikbugs fixen kann. Wenn aktiviert, ist die Leistung reduziert.</string> + <string name="asynchronous_gpu">Asynchrone GPU-Emulation aktivieren</string> + <string name="asynchronous_gpu_description">Verwendet einen separaten Thread, um die GPU asynchron zu emulieren. Wenn aktiviert, wird die Leistung verbessert.</string> + <string name="frame_limit_enable">Geschwindigkeitsbeschränkung aktivieren</string> + <string name="frame_limit_enable_description">Wenn aktiviert, wird die Emulationsgeschwindigkeit auf einen angegebenen Prozentsatz der normalen Geschwindigkeit begrenzt.</string> + <string name="frame_limit_slider">Prozentsatz der Geschwindigkeitsbeschränkung</string> + <string name="frame_limit_slider_description">Gibt den Prozentsatz zur Begrenzung der Emulationsgeschwindigkeit an. Mit der Voreinstellung von 100% wird die Emulation auf normale Geschwindigkeit begrenzt. Höhere oder niedrigere Werte erhöhen oder verringern die Geschwindigkeitsbeschränkung.</string> + <string name="internal_resolution">Interne Auflösung</string> + <string name="internal_resolution_description">Legt die Renderauflösung fest. Eine hohe Auflösung wird die Grafikqualität stark verbessern, aber sich negativ auf die Leistung auswirken und möglicherweise Bugs in einigen Spielen verursachen.</string> + <string name="performance_warning">Das Ausschalten dieser Einstellung wird die Emulationsleistung erheblich verringern! Für die beste Erfahrung wird empfohlen, diese Einstellung aktiviert zu lassen.</string> + <string name="debug_warning">Warnung: Das Ändern dieser Einstellungen wird die Emulation verlangsamen</string> + + <!-- Premium strings --> + <string name="premium_text">Premium</string> + <string name="premium_settings_upsell">Upgraden sie auf Premium und unterstützen sie Citra!</string> + <string name="premium_settings_upsell_description">Mit Premium helfen sie den Entwicklern, Citra weiter zu verbessen und erhalten Zugriff auf diese exklusiven Funktionen!</string> + <string name="premium_settings_welcome">Willkommen zu Premium.</string> + <string name="premium_settings_welcome_description">Vielen Dank für die Unterstützung!</string> + + <!-- Audio settings strings --> + <string name="audio_stretch">Audiodehnung aktivieren</string> + <string name="audio_stretch_description">Dehnt Audio um Stottern zu reduzieren. Wenn aktiviert, wird die Audiolatenz erhöht und die Leistung leicht verschlechtert.</string> + + <!-- Miscellaneous --> + <string name="clear">Zurücksetzen</string> + <string name="slider_default">Standard</string> + <string name="ini_saved">Einstellungen gespeichert</string> + <string name="gameid_saved">Einstellungen gespeichert für %1$s</string> + <string name="error_saving">Fehler beim Speicher von %1$s.ini: %2$s</string> + + <!-- Game Grid Screen--> + <string name="grid_menu_core_settings">Einstellungen</string> + + <!-- Add Directory Screen--> + <string name="select_game_folder">Spieleordner auswählen</string> + <string name="add_directory_title">Ordner zur Bibliothek hinzufügen</string> + + <!-- Preferences Screen --> + <string name="preferences_settings">Einstellungen</string> + <string name="preferences_premium">Premium</string> + <string name="preferences_general">Allgemein</string> + <string name="preferences_system">System</string> + <string name="preferences_camera">Kamera</string> + <string name="preferences_controls">Gamepad</string> + <string name="preferences_graphics">Grafik</string> + <string name="preferences_audio">Audio</string> + <string name="preferences_debug">Debug</string> + + <!-- ROM loading errors --> + <string name="loader_error_encrypted">Das ROM ist verschlüsselt</string> + <string name="loader_error_invalid_format">Ungültiges ROM-Format</string> + + <!-- Emulation Menu --> + <string name="emulation_show_fps">FPS anzeigen</string> + <string name="emulation_configure_controls">Steuerung konfigurieren</string> + <string name="emulation_edit_layout">Layout editieren</string> + <string name="emulation_done">Fertig</string> + <string name="emulation_toggle_controls">Knöpfe ein-/ausschalten</string> + <string name="emulation_control_scale">Größe anpassen</string> + <string name="emulation_open_settings">Einstellungen öffnen</string> + <string name="emulation_switch_screen_layout">Querformat-Bildschirmlayout</string> + <string name="emulation_screen_layout_landscape">Standard</string> + <string name="emulation_screen_layout_portrait">Hochformat</string> + <string name="emulation_screen_layout_single">Einzelbildschirm</string> + <string name="emulation_screen_layout_sidebyside">Bildschirme Seite an Seite</string> + <string name="emulation_swap_screens">Bildschirme tauschen</string> + <string name="emulation_touch_overlay_reset">Overlay zurücksetzen</string> + <string name="emulation_show_overlay">Overlay anzeigen</string> + <string name="emulation_close_game">Spiel schließen</string> + <string name="emulation_close_game_message">Sind sie sicher, dass sie das Spiel schließen wollen?</string> + <string name="menu_emulation_amiibo">Amiibo</string> + <string name="menu_emulation_amiibo_load">Laden</string> + <string name="menu_emulation_amiibo_remove">Entfernen</string> + <string name="select_amiibo">Amiibo-Datei auswählen</string> + <string name="amiibo_load_error">Fehler beim Laden der Amiibo</string> + <string name="amiibo_load_error_message">Beim Laden der Amiibo-Datei ist ein Fehler aufgetreten. Bitte überprüfen sie, ob die Datei korrekt ist.</string> + + <string name="write_permission_needed">Schreibzugriff auf den externen Speicher muss erlaubt sein, damit der Emulator funktioniert</string> + <string name="load_settings">Lade Einstellungen...</string> + + <string name="external_storage_not_mounted">Der externe Speicher muss verfügbar sein, um Citra nutzen zu können</string> + + <string name="select_dir">Diesen Ordner auswählen</string> + <string name="empty_gamelist">Keine Dateien wurden gefunden oder kein Spieleordner wurde ausgewählt.</string> + + <!-- Software Keyboard --> + <string name="software_keyboard">Software-Tastatur</string> + <string name="i_forgot">Ich habs vergessen</string> + <string name="fixed_length_required">Textlänge ist nicht korrekt (sollte %d Buchstaben lang sein)</string> + <string name="max_length_exceeded">Text ist zu lang (sollte nicht länger als %d Buchstaben sein)</string> + <string name="blank_input_not_allowed">Leere Eingabe is nicht erlaubt</string> + <string name="empty_input_not_allowed">Leere Eingabe is nicht erlaubt</string> + + <!-- Mii Selector --> + <string name="mii_selector">Mii-Selektor</string> + <string name="standard_mii">Standard Mii</string> + + <!-- Camera --> + <string name="camera_select_image">Bild auswählen</string> + <string name="camera">Kamera</string> + <string name="camera_permission_needed">Citra muss auf Ihre Kamera zugreifen, um die Kameras der 3DS zu emulieren.\n\nAlternativ können Sie auch die \"Bildquelle\" in den Kameraeinstellungen auf \"Standbild\" setzen.</string> +</resources> diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml new file mode 100644 index 000000000..448d90e87 --- /dev/null +++ b/src/android/app/src/main/res/values-es/strings.xml @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_disclaimer">Este software corre juegos de la Nintendo 3DS. Los juegos no vienen incluidos.\n\nAntes de comenzar, por favor, pon los juegos de 3DS que poseas legalmente en el almacenamiento del dispositivo.</string> + <string name="app_notification_channel_description">Notificaciones del Emulador Citra 3DS</string> + <string name="app_notification_running">Citra está ejecutándose</string> + + <!-- Input related strings --> + <string name="controller_circlepad">Pad Circular</string> + <string name="controller_c">Palanca C</string> + <string name="controller_triggers">Botones Traseros</string> + <string name="controller_dpad">Pad de Control</string> + <string name="controller_axis_vertical">Eje Vertical</string> + <string name="controller_axis_horizontal">Eje Horizontal</string> + <string name="input_binding">Asignación de botones</string> + <string name="input_binding_description">Pulsa o mueve un botón para asignarlo a %1$s.</string> + <string name="input_binding_description_vertical_axis">Mueve el joystick arriba o abajo.</string> + <string name="input_binding_description_horizontal_axis">Mueve el joystick a izquierda o derecha.</string> + <string name="input_message_analog_only">¡Este control debe asignarse a un stick analógico del mando o a un eje del Pad de Control!</string> + <string name="input_message_button_only">¡Este control debe asignarse a un botón del mando!</string> + + <!-- Generic buttons (Shared with lots of stuff) --> + <string name="generic_buttons">Botones</string> + + <!-- Premium settings strings --> + <string name="design">Cambiar Tema (Claro, Oscuro)</string> + <string name="design_updated">El tema se actualizará cuando salgas de la Configuración</string> + + <!-- Core settings strings --> + <string name="cpu_jit">Activar CPU JIT</string> + <string name="cpu_jit_description">Usa el compilador Just-In-Time (JIT) para la emulación de la CPU. Cuando se active, el rendimiento mejorará notablemente.</string> + <string name="init_clock">Tipo del reloj del sistema</string> + <string name="init_clock_description">Configura el reloj emulado de la 3DS para que tenga la misma fecha y hora de tu dispositivo o para configurar una fecha y hora distinta en éste.</string> + + <!-- System settings strings --> + <string name="init_time">Sobreescritura del tiempo inicial del reloj del sistema</string> + <string name="init_time_description">Si el \"Tipo del reloj del sistema\" está en \"Reloj emulado\", ésto cambia la fecha y hora de inicio.</string> + <string name="emulated_region">Región emulada</string> + <string name="emulated_language">Idioma emulado</string> + + <!-- Camera settings strings --> + <string name="inner_camera">Cámara interior</string> + <string name="outer_left_camera">Cámara izquierda externa</string> + <string name="outer_right_camera">Cámara derecha externa</string> + <string name="image_source">Fuente de la imagen</string> + <string name="image_source_description">Configura la fuente de la imagen de la cámara virtual. Puedes usar un archivo de imagen, o una cámara del dispositivo si es soportada.</string> + <string name="camera_device">Cámara del dispositivo</string> + <string name="camera_device_description">Si la \"Fuente de la imagen\" está en \"Cámara del dispositivo\", ésto usará la cámara del propio dispositivo.</string> + <string name="camera_facing_front">Frontal</string> + <string name="camera_facing_back">Trasera</string> + <string name="camera_facing_external">Externa</string> + <string name="image_flip">Imagen volteada</string> + + <!-- Graphics settings strings --> + <string name="vsync">Activar Sincronización Vertical</string> + <string name="vsync_description">Sincroniza los cuadros por segundo del juego con la tasa de refresco de tu dispositivo.</string> + <string name="linear_filtering">Activar filtro linear</string> + <string name="linear_filtering_description">Activa el filtro linear, que hace que los gráficos del juego se vean más suaves.</string> + <string name="texture_filter_name">Filtro de Texturas</string> + <string name="texture_filter_description">Mejora los gráficos visuales de los juegos al aplicar un filtro a las texturas. Los filtros soportados son Anime4K Ultrafast, Bicubic, ScaleForce, y xBRZ freescale.</string> + <string name="hw_renderer">Activar renderizador de hardware</string> + <string name="hw_renderer_description">Usa el hardware para emular los gráficos de 3DS. Cuando se active, el rendimiento mejorará notablemente.</string> + <string name="hw_shaders">Activar sombreador de hardware</string> + <string name="hw_shaders_description">Usa el hardware para emular los sombreadores de 3DS. Cuando se active, el rendimiento mejorará notablemente.</string> + <string name="shaders_accurate_mul">Activar multiplicación precisa de sombreado</string> + <string name="shaders_accurate_mul_description">Usa multiplicaciones más precisas en los sombreados de hardware, que podrían arreglar ciertos bugs gráficos. Cuando se active, el rendimiento se reducirá.</string> + <string name="asynchronous_gpu">Activar Emulación Asíncrona de la GPU</string> + <string name="asynchronous_gpu_description">Usa un hilo separado para emular la GPU de manera asíncrona. Cuando se active, el rendimiento mejorará.</string> + <string name="frame_limit_enable">Activar límite de velocidad</string> + <string name="frame_limit_enable_description">Cuando se active, la velocidad de emulación estará limitada a un porcentaje determinado de la velocidad normal.</string> + <string name="frame_limit_slider">Limitar porcentaje de velocidad</string> + <string name="frame_limit_slider_description">Especifica el valor al que se limita la velocidad de emulación. Con el valor por defecto del 100%, la emulación se limitará a la velocidad normal. Los valores altos o altos incrementarán o reducirán el límite de velocidad.</string> + <string name="internal_resolution">Resolución interna</string> + <string name="internal_resolution_description">Especifica la resolución a la que se quiera renderizar. Una alta resolución mejorará la calidad visual un montón, pero también causará un gran impacto en el rendimiento y puede causar fallos en ciertos juegos.</string> + <string name="performance_warning">¡Desactivar esta opción reducirá la velocidad de emulación! Para obtener la mejor experiencia, se recomienda que se mantenga activada.</string> + <string name="debug_warning">Aviso: Modificar estas configuraciones reducirán la velocidad de emulación.</string> + + <!-- Premium strings --> + <string name="premium_text">Premium</string> + <string name="premium_settings_upsell">¡Actualízate al Premium y apoya a Citra!</string> + <string name="premium_settings_upsell_description">Con el Premium, ¡apoyarás a los desarrolladores para seguir mejorando Citra, y ganarás acceso a características exclusivas!</string> + <string name="premium_settings_welcome">Bienvenid@ al Premium.</string> + <string name="premium_settings_welcome_description">¡Gracias por tu apoyo!</string> + + <!-- Audio settings strings --> + <string name="audio_stretch">Activar extensión de audio</string> + <string name="audio_stretch_description">Extiende el audio para reducir los parones. Cuando se active, la latencia de audio se incrementará y reducirá un poco el rendimiento.</string> + + <!-- Miscellaneous --> + <string name="clear">Reiniciar</string> + <string name="slider_default">Por defecto</string> + <string name="ini_saved">Configuración guardada</string> + <string name="gameid_saved">Configuración guardada para %1$s</string> + <string name="error_saving">Error guardando %1$s.ini: %2$s</string> + + <!-- Game Grid Screen--> + <string name="grid_menu_core_settings">Configuración</string> + + <!-- Add Directory Screen--> + <string name="select_game_folder">Seleccionar Directorio de Juego</string> + <string name="add_directory_title">Añadir Carpeta a la Librería de Juegos</string> + + <!-- Preferences Screen --> + <string name="preferences_settings">Configuración</string> + <string name="preferences_premium">Premium</string> + <string name="preferences_general">General</string> + <string name="preferences_system">Sistema</string> + <string name="preferences_camera">Cámara</string> + <string name="preferences_controls">Controles</string> + <string name="preferences_graphics">Gráficos</string> + <string name="preferences_audio">Audio</string> + <string name="preferences_debug">Depuración</string> + + <!-- ROM loading errors --> + <string name="loader_error_encrypted">Tu ROM está encriptada</string> + <string name="loader_error_invalid_format">Formato de ROM no válido</string> + + <!-- Emulation Menu --> + <string name="emulation_show_fps">Mostrar FPS</string> + <string name="emulation_configure_controls">Configurar Controles</string> + <string name="emulation_edit_layout">Editar Estilo</string> + <string name="emulation_done">Hecho</string> + <string name="emulation_toggle_controls">Activar Controles</string> + <string name="emulation_control_scale">Ajustar Escala</string> + <string name="emulation_open_settings">Abrir Configuración</string> + <string name="emulation_switch_screen_layout">Estilo de Pantalla Apaisada</string> + <string name="emulation_screen_layout_landscape">Por defecto</string> + <string name="emulation_screen_layout_portrait">Retrato</string> + <string name="emulation_screen_layout_single">Pantalla Única</string> + <string name="emulation_screen_layout_sidebyside">Conjunta</string> + <string name="emulation_swap_screens">Intercambiar Pantallas</string> + <string name="emulation_touch_overlay_reset">Reiniciar Estilo</string> + <string name="emulation_show_overlay">Mostrar Estilo</string> + <string name="emulation_close_game">Cerrar Juego</string> + <string name="emulation_close_game_message">¿Estás seguro de querer cerrar el juego?</string> + <string name="menu_emulation_amiibo">Amiibo</string> + <string name="menu_emulation_amiibo_load">Cargar</string> + <string name="menu_emulation_amiibo_remove">Quitar</string> + <string name="select_amiibo">Seleccionar archivo de Amiibo</string> + <string name="amiibo_load_error">Error al cargar el Amiibo</string> + <string name="amiibo_load_error_message">Durante la carga del archivo del Amiibo, ocurrió un error. Por favor, asegúrese de que el archivo es el correcto.</string> + + <string name="write_permission_needed">Necesitas dar permisos de escritura al almacenamiento externo para que funcione el emulador.</string> + <string name="load_settings">Cargando la configuración...</string> + + <string name="external_storage_not_mounted">El almacenamiento externo necesita estar disponible para poder usar Citra.</string> + + <string name="select_dir">Selecciona Este Directorio</string> + <string name="empty_gamelist">No se han encontrado archivos o todavía no se ha elegido el directorio de juegos.</string> + + <!-- Software Keyboard --> + <string name="software_keyboard">Teclado de Software</string> + <string name="i_forgot">Lo Olvidé</string> + <string name="fixed_length_required">La longitud del texto no es correcta (debe tener %d caracteres)</string> + <string name="max_length_exceeded">El texto es muy largo (no debe tener más de%d caracteres)</string> + <string name="blank_input_not_allowed">No puedes dejarlo en blanco</string> + <string name="empty_input_not_allowed">No se puede dejar en blanco</string> + + <!-- Mii Selector --> + <string name="mii_selector">Selector de Miis</string> + <string name="standard_mii">Mii estándar</string> + + <!-- Camera --> + <string name="camera_select_image">Seleccionar Imagen</string> + <string name="camera">Cámara</string> + <string name="camera_permission_needed">Citra necesita permiso para acceder a tu cámara para emular las cámaras de la 3DS.\n\nO también puedes poner la \"Fuente de la imagen\" en \"Imagen normal\" en la Configuración de la Cámara.</string> +</resources> diff --git a/src/android/app/src/main/res/values-fi/strings.xml b/src/android/app/src/main/res/values-fi/strings.xml new file mode 100644 index 000000000..dfdfdc88d --- /dev/null +++ b/src/android/app/src/main/res/values-fi/strings.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_disclaimer">Tämä sovellus pelaa Nintendo 3DS-käsikonsolin pelejä. Pelejä itse ei tule sovelluksen mukana.\n\nEnnen kuin jatkat, laita itse omistamasi 3DS-pelien pelitiedostot laitteesi tallennustilaan.</string> + <string name="app_notification_channel_description">Citra 3DS-emulaattorin ilmoitukset</string> + <string name="app_notification_running">Citra on päällä</string> + + <string name="controller_c">C-Tikku</string> + <string name="controller_triggers">Liipaisimet</string> + <string name="controller_dpad">D-Pad</string> + <string name="controller_axis_vertical">Ylä/ala-akseli</string> + <string name="controller_axis_horizontal">Vasen/oikea akseli</string> + <string name="input_binding_description_vertical_axis">Liikuta tattia ylös tai alas.</string> + <string name="input_binding_description_horizontal_axis">Liikuta tattia vasemmalle tai oikealle.</string> + <!-- Generic buttons (Shared with lots of stuff) --> + <string name="generic_buttons">Nappulat</string> + + <!-- Premium settings strings --> + <string name="design">Vaihda teema (Vaalea, Tumma)</string> + <string name="design_updated">Teema päivittyy kun asetukset suljetaan</string> + + <!-- Core settings strings --> + <string name="cpu_jit">Aktivoi CPU JIT</string> + <string name="cpu_jit_description">Käyttää Just-in-Time (JIT) kääntäjää prosessorin emulointiin. Kun tämä on päällä, pelien suorituskyky on huomattavasti parempi.</string> + <string name="init_clock">Järjestelmän kellotyyppi</string> + <string name="init_clock_description">Aseta emuloidun 3DS:n kello samaksi kuin laitteesi kello, tai aloita kello simuloidusta päivämäärästä ja ajasta.</string> + + <!-- System settings strings --> + <string name="init_time">Järjestelmän kellon aloitusajan yliajo.</string> + <string name="emulated_region">Emuloitu alue</string> + <string name="emulated_language">Emuloitu kieli</string> + + <!-- Camera settings strings --> + <string name="inner_camera">Sisäkamera</string> + <string name="outer_left_camera">Vasen Ulkokamera</string> + <string name="outer_right_camera">Oikea Ulkokamera</string> + <string name="image_source">Kuvalähde</string> + <string name="image_source_description">Asettaa kuvalähteen virtuaalikameralle. Voit käyttää kuvaa tai laitteen kameraa, jos sitä tuetaan.</string> + <string name="camera_device">Kameralaite</string> + <string name="camera_device_description">Kos \"Kuvalähde\"-asetus on \"Laitteen Kamera\", tämä asettaa kameran jota käytetään.</string> + <string name="camera_facing_front">Etupuoli</string> + <string name="camera_facing_back">Takapuoli</string> + <string name="camera_facing_external">Ulkoinen</string> + <string name="image_flip">Kuvan kääntö</string> + + <!-- Graphics settings strings --> + <string name="vsync">Aktivoi V-Sync</string> + <string name="vsync_description">Synkronoi pelin virkistystaajuus laitteesi virkistystaajuuteen.</string> + <string name="hw_renderer">Aktivoi Laitteistorenderöinti</string> + <string name="hw_renderer_description">Käyttää laitteistoa emuloidakseen 3DS-grafiikoita. Kun tämä on päällä, pelien suorituskyky on huomattavasti parempi.</string> + <string name="hw_shaders">Aktivoi Laitteistovarjostin</string> + <string name="hw_shaders_description">Käyttää laitteistoa emuloidakseen 3DS:n varjostimia. Kun tämä on päällä, pelien suorituskyky on huomattavasti parempi.</string> + <string name="frame_limit_enable">Aktivoi nopeuden rajoitus</string> + <string name="frame_limit_enable_description">Kun aktivoitu, emulaation nopeus on rajoitettu asetettuun prosenttiin normaalista nopeudesta.</string> + <string name="frame_limit_slider">Rajoita nopeusprosenttia</string> + <!-- Audio settings strings --> + <string name="audio_stretch">Aktivoi äänen venytys</string> + <!-- Miscellaneous --> + <string name="clear">Tyhjennä</string> + <string name="slider_default">Oletus</string> + <!-- Game Grid Screen--> + <string name="grid_menu_core_settings">Asetukset</string> + + <!-- Add Directory Screen--> + <string name="select_game_folder">Valitse pelikansio</string> + <string name="add_directory_title">Lisää kansio kirjastoosi</string> + + <!-- Preferences Screen --> + <string name="preferences_settings">Asetukset</string> + <string name="preferences_general">Yleinen</string> + <string name="preferences_system">Laite</string> + <string name="preferences_controls">Ohjain</string> + <string name="preferences_graphics">Grafiikat</string> + <string name="preferences_audio">Ääni</string> + <!-- ROM loading errors --> + <string name="loader_error_encrypted">Pelitiedostosi on salattu.</string> + <string name="loader_error_invalid_format">Epäsopiva pelitiedoston formaatti</string> + + <!-- Emulation Menu --> + <string name="emulation_show_fps">Näytä FPS</string> + <string name="emulation_done">Valmis</string> + <string name="emulation_open_settings">Avaa asetukset</string> + <string name="emulation_screen_layout_landscape">Oletus</string> + <string name="emulation_screen_layout_single">Yksi näyttö</string> + <string name="emulation_screen_layout_sidebyside">Näytöt vierekkäin</string> + <string name="emulation_swap_screens">Vaihda näytöt</string> + <string name="emulation_close_game">Sulje peli</string> + <string name="emulation_close_game_message">Oletko varma että haluat sulkea nykyisen pelin?</string> + <string name="load_settings">Ladataan asetuksia...</string> + + <!-- Software Keyboard --> + <string name="software_keyboard">Laitteistonäppäimistö</string> + <string name="i_forgot">Unohdin</string> + <string name="fixed_length_required">Tekstin pituus ei ole oikein (pitäisi olla %d merkkiä) </string> + <string name="max_length_exceeded">Teksti on liian pitkä (ei pitäisi olla enempää kuin %d merkkiä)</string> + </resources> diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..65a21f066 --- /dev/null +++ b/src/android/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_disclaimer">Ce logiciel fera fonctionner des jeux de la console portable Nintendo 3DS. Aucun jeu n\'est inclus.\n\nAvant de démarrer, veuillez mettre vos fichiers de jeux 3DS acquis de droit sur votre périphérique de stockage.</string> + <string name="app_notification_channel_description">Notifications de l\'émulateur 3DS Citra</string> + <string name="app_notification_running">Citra est en fonctionnement</string> + + <!-- Input related strings --> + <string name="controller_circlepad">Circle Pad</string> + <string name="controller_c">C-Stick</string> + <string name="controller_triggers">Gachettes</string> + <string name="controller_dpad">Croix directionnelle</string> + <string name="controller_axis_vertical">Axe Haut/Bas</string> + <string name="controller_axis_horizontal">Axe Gauche/Droite</string> + <string name="input_binding">Rattachement de saisie</string> + <string name="input_binding_description">Faites une saisie par pression ou déplacement pour la rattacher à %1$s.</string> + <string name="input_binding_description_vertical_axis">Déplacez votre joystick vers le haut ou vers le bas.</string> + <string name="input_binding_description_horizontal_axis">Déplacez votre joystick à gauche ou à droite.</string> + <string name="input_message_analog_only">Cette commande doit être rattachée à un stick analogique de manette ou à un axe de la croix directionnelle !</string> + <string name="input_message_button_only">Cette commande doit être rattachée à un bouton de manette !</string> + + <!-- Generic buttons (Shared with lots of stuff) --> + <string name="generic_buttons">Boutons</string> + + <!-- Premium settings strings --> + <string name="design">Changer de thème (Clair, sombre)</string> + <string name="design_updated">Le thème sera mis à jour lorsque vous quitterez les paramètres</string> + + <!-- Core settings strings --> + <string name="cpu_jit">Activer le CPU JIT</string> + <string name="cpu_jit_description">Utilise le compilateur Just-in-Time (JIT) pour l\'émulation CPU. Lorsqu\'il est activé, la performance des jeux sera améliorée de manière significative.</string> + <string name="init_clock">Type de l\'horloge système</string> + <string name="init_clock_description">Définissez l\'horloge émulée de la 3DS pour qu\'elle corresponde à votre appareil ou démarrez à une date et heure simulée.</string> + + <!-- System settings strings --> + <string name="init_time">Forcer l\'heure de départ de l\'horloge système</string> + <string name="init_time_description">Si le paramètre \"type de l\'horloge système\" est sur \"Horloge simulée\", cette option modifiera la date et l\'heure fixées auxquelles démarrer.</string> + <string name="emulated_region">Région d\'émulation</string> + <string name="emulated_language">Langue d\'émulation</string> + + <!-- Camera settings strings --> + <string name="inner_camera">Caméra intérieure</string> + <string name="outer_left_camera">Caméra extérieure de gauche</string> + <string name="outer_right_camera">Caméra extérieure de droite</string> + <string name="image_source">Image source</string> + <string name="image_source_description">Définit l\'image source de la caméra virtuelle. Vous pouvez utiliser un fichier image ou un périphérique vidéo s\'il est supporté.</string> + <string name="camera_device">Périphérique vidéo de la caméra</string> + <string name="camera_device_description">Si le paramètre \"image source\" est configuré sur \"Périphérique vidéo de la caméra\", vous pourrez définir la caméra physique qui sera utilisée. </string> + <string name="camera_facing_front">Avant</string> + <string name="camera_facing_back">Arrière</string> + <string name="camera_facing_external">Extérieure</string> + <string name="image_flip">Basculement de l\'image</string> + + <!-- Graphics settings strings --> + <string name="vsync">Activer la synchronisation verticale (VSync)</string> + <string name="vsync_description">Synchronise la fréquence d\'images du jeu avec la fréquence de rafraîchissement de votre appareil.</string> + <string name="linear_filtering">Activer le filtrage linéaire</string> + <string name="linear_filtering_description">Active le filtrage linéaire, qui améliorera le lissage graphique du jeu.</string> + <string name="texture_filter_name">Filtrage des textures</string> + <string name="texture_filter_description">Améliore l\'aspect graphique des jeux en appliquant un filtre aux textures. Les filtres supportés sont Anime4K Ultrafast, Bicubic, ScaleForce, et xBRZ freescale.</string> + <string name="hw_renderer">Activer le rendu matériel</string> + <string name="hw_renderer_description">Utilise le matériel pour émuler les graphismes de la 3DS. Lorsqu\'il est activé, la performance des jeux sera améliorée de manière significative.</string> + <string name="hw_shaders">Activer le shader (nuanceur) matériel </string> + <string name="hw_shaders_description">Utilise le matériel pour émuler les shaders de la 3DS. Lorsqu\'il est activé, la performance des jeux sera améliorée de manière significative.</string> + <string name="shaders_accurate_mul">Activer la multiplication précise dans les shaders</string> + <string name="shaders_accurate_mul_description">Utilise une multiplication plus précise dans les shaders matériels, ce qui peut corriger certains bugs graphiques. Lorsqu\'elle est activée, la performance sera réduite.</string> + <string name="asynchronous_gpu">Active l\'émulation asynchone du GPU</string> + <string name="asynchronous_gpu_description">Utilise une unité d’exécution séparée pour émuler le GPU de manière asynchrone. La performance sera améliorée.</string> + <string name="frame_limit_enable">Activer la limitation de vitesse</string> + <string name="frame_limit_enable_description">Lorsqu\'elle est activée, la vitesse d\'émulation sera limitée par un pourcentage spécifique de la vitesse normale.</string> + <string name="frame_limit_slider">Pourcentage de la limitation de vitesse</string> + <string name="frame_limit_slider_description">Définit le taux de limitation de la vitesse d\'émulation. A 100%, par défaut, l\'émulation sera limitée à la vitesse normale. Des valeurs supérieures ou inférieures augmenteront ou diminueront la limite de la vitesse.</string> + <string name="internal_resolution">Définition interne</string> + <string name="internal_resolution_description">Spécifie la définition utilisée pour le rendu. Une définition élevée améliorera de beaucoup la qualité graphique mais aura un impact important sur les performances et pourra créer des bugs dans certains jeux.</string> + <string name="performance_warning">Désactiver cette option va considérablement réduire la performance de l\'émulateur ! Pour bénéficier de la meilleure expérience, nous vous recommandons de laisser ce paramètre activé.</string> + <string name="debug_warning">Attention: Modifier ces paramètres ralentira l\'émulation</string> + + <!-- Premium strings --> + <string name="premium_text">Premium</string> + <string name="premium_settings_upsell">Passez à un compte Premium et soutenez Citra !</string> + <string name="premium_settings_upsell_description">Avec un compte Premium, vous soutiendrez les développeurs pour qu\'ils continuent à améliorer Citra, et vous aurez accès à des fonctions exclusives!</string> + <string name="premium_settings_welcome">Bienvenue à l\'accès Premium.</string> + <string name="premium_settings_welcome_description">Merci pour votre soutien !</string> + + <!-- Audio settings strings --> + <string name="audio_stretch">Activer l\'étirement audio</string> + <string name="audio_stretch_description">Étire le son pour réduire les saccades. Lorsqu\'il est activé, la latence est augmentée et les performances sont légèrement réduites.</string> + + <!-- Miscellaneous --> + <string name="clear">Effacer</string> + <string name="slider_default">Par défaut</string> + <string name="ini_saved">Paramètres sauvegardés</string> + <string name="gameid_saved">Paramètres sauvegardés pour %1$s</string> + <string name="error_saving">Erreur lors de l\'enregistrement de %1$s.ini: %2$s</string> + + <!-- Game Grid Screen--> + <string name="grid_menu_core_settings">Paramètres</string> + + <!-- Add Directory Screen--> + <string name="select_game_folder">Choisir un répertoire de jeu</string> + <string name="add_directory_title">Ajouter un répertoire à la bibliothèque</string> + + <!-- Preferences Screen --> + <string name="preferences_settings">Paramètres</string> + <string name="preferences_premium">Premium</string> + <string name="preferences_general">Généralités</string> + <string name="preferences_system">Système</string> + <string name="preferences_camera">Caméra</string> + <string name="preferences_controls">Manette de jeu</string> + <string name="preferences_graphics">Graphismes</string> + <string name="preferences_audio">Son</string> + <string name="preferences_debug">Débogguer</string> + + <!-- ROM loading errors --> + <string name="loader_error_encrypted">Votre ROM est chiffrée</string> + <string name="loader_error_invalid_format">Format de ROM non valide</string> + + <!-- Emulation Menu --> + <string name="emulation_show_fps">Afficher les FPS</string> + <string name="emulation_configure_controls">Configurer les commandes</string> + <string name="emulation_edit_layout">Éditer la disposition</string> + <string name="emulation_done">Fait</string> + <string name="emulation_toggle_controls">Alterner les commandes</string> + <string name="emulation_control_scale">Ajuster l\'échelle</string> + <string name="emulation_open_settings">Ouvrir les paramètres</string> + <string name="emulation_switch_screen_layout">Disposition de l\'écran en mode paysage</string> + <string name="emulation_screen_layout_landscape">Par défaut</string> + <string name="emulation_screen_layout_portrait">Mode portrait</string> + <string name="emulation_screen_layout_single">Un seul écran</string> + <string name="emulation_screen_layout_sidebyside">Écrans côte à côte</string> + <string name="emulation_swap_screens">Permuter les écrans</string> + <string name="emulation_touch_overlay_reset">Remettre à zéro l\'Overlay (élément en superposition)</string> + <string name="emulation_show_overlay">Afficher l\'Overlay</string> + <string name="emulation_close_game">Fermer le jeu</string> + <string name="emulation_close_game_message">Êtes-vous sûr de vouloir fermer le jeu en cours ?</string> + <string name="menu_emulation_amiibo">Amiibo</string> + <string name="menu_emulation_amiibo_load">Charger</string> + <string name="menu_emulation_amiibo_remove">Supprimer</string> + <string name="select_amiibo">Sélection du fichier Amiibo</string> + <string name="amiibo_load_error">Erreur lors du chargement de Amiibo</string> + <string name="amiibo_load_error_message">Lors du chargement du fichier Amiibo spécifié, une erreur s\'est produite. Veuillez vérifier que le fichier est convenable.</string> + + <string name="write_permission_needed">Vous devez autoriser l\'accès en écriture à un stockage externe pour que l\'émulateur fonctionne</string> + <string name="load_settings">Chargement des paramètres...</string> + + <string name="external_storage_not_mounted">Le stockage externe doit être disponible pour utiliser Citra</string> + + <string name="select_dir">Choisissez ce répertoire</string> + <string name="empty_gamelist">Aucun fichier n\'a été trouvé ou aucun répertoire de jeu n\'a encore été sélectionné.</string> + + <!-- Software Keyboard --> + <string name="software_keyboard">Clavier logiciel</string> + <string name="i_forgot">J\'ai oublié</string> + <string name="fixed_length_required">La longueur du texte est incorrecte (elle devrait être de %dcaractères)</string> + <string name="max_length_exceeded">Le texte est trop long (il ne devrait pas faire plus de %d caractères)</string> + <string name="blank_input_not_allowed">Une entrée vide n\'est pas autorisée</string> + <string name="empty_input_not_allowed">Une entrée vide n\'est pas autorisée</string> + + <!-- Mii Selector --> + <string name="mii_selector">Sélectionneur de Mii</string> + <string name="standard_mii">Mii standard</string> + + <!-- Camera --> + <string name="camera_select_image">Sélection de l\'image</string> + <string name="camera">Caméra</string> + <string name="camera_permission_needed">Citra a besoin d\'accéder à votre caméra pour émuler les caméras de la 3DS.\n\nSinon, vous pouvez définir \"Image Source\" sur \"Image fixe\" dans les paramètres de la caméra.</string> +</resources> diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml new file mode 100644 index 000000000..c1aa19da6 --- /dev/null +++ b/src/android/app/src/main/res/values-it/strings.xml @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_disclaimer">Questo software avvierà giochi per la console portatile Nintendo 3DS. Non è incluso alcun gioco.\n\nPrima di avviarlo, si prega di inserire i file dei giochi 3DS legalmente ottenuti nella memoria del tuo dispositivo.</string> + <string name="app_notification_channel_description">Notifiche dell\'emulatore 3DS Citra</string> + <string name="app_notification_running">Citra è in esecuzione</string> + + <!-- Input related strings --> + <string name="controller_circlepad">Pad Scorrevole</string> + <string name="controller_c">Stick C</string> + <string name="controller_triggers">Pulsanti Dorsali</string> + <string name="controller_dpad">Pad Direzionale</string> + <string name="controller_axis_vertical">Asse Verticale</string> + <string name="controller_axis_horizontal">Asse Orizzontale</string> + <string name="input_binding">Assegnamento Input</string> + <string name="input_binding_description">Premi o muovi un pulsante per assegnarlo a %1$s.</string> + <string name="input_binding_description_vertical_axis">Muovi il joystick in su o in giù.</string> + <string name="input_binding_description_horizontal_axis">Muovi il joystick a sinistra o a destra.</string> + <string name="input_message_analog_only">Questo controllo dev\'essere assegnato ad uno stick analogico di un gamepad o ad un asse del Pad Direzionale!</string> + <string name="input_message_button_only">Questo controllo dev\'essere assegnato al pulsante di un gamepad!</string> + + <!-- Generic buttons (Shared with lots of stuff) --> + <string name="generic_buttons">Pulsanti</string> + + <!-- Premium settings strings --> + <string name="design">Cambia Tema (Chiaro, Scuro)</string> + <string name="design_updated">Il tema verrà aggiornato all\'uscita dalle Impostazioni</string> + + <!-- Core settings strings --> + <string name="cpu_jit">Abilita CPU JIT</string> + <string name="cpu_jit_description">Utilizza il compilatore Just-in-Time (JIT) per l\'emulazione della CPU. Se abilitato, le prestazioni di gioco miglioreranno significativamente.</string> + <string name="init_clock">Tipo di orologio di sistema</string> + <string name="init_clock_description">Imposta l\'orologio emulato del 3DS o per rispecchiare l\'orario del tuo dispositivo o per configurare una data e ora in particolare.</string> + + <!-- System settings strings --> + <string name="init_time">Sovrascrittura dell\'orario iniziale dell\'orologio di sistema</string> + <string name="init_time_description">Se \"Tipo di orologio di sistema\" è impostato su \"Orologio simulato\", da qui puoi modificare la data e l\'ora di gioco.</string> + <string name="emulated_region">Regione emulata</string> + <string name="emulated_language">Lingua emulata</string> + + <!-- Camera settings strings --> + <string name="inner_camera">Fotocamera Interna</string> + <string name="outer_left_camera">Fotocamera Esterna di Sinistra</string> + <string name="outer_right_camera">Fotocamera Esterna di Destra</string> + <string name="image_source">Immagine Sorgente</string> + <string name="image_source_description">Imposta l\'immagine sorgente della fotocamera virtuale. Puoi utilizzare un file immagine, oppure la fotocamera del dispositivo quando supportato.</string> + <string name="camera_device">Fotocamera del Dispositivo</string> + <string name="camera_device_description">Se \"Immagine Sorgente\" è impostato su \"Fotocamera del Dispositivo\", questo utilizzerà la fotocamera del dispositivo.</string> + <string name="camera_facing_front">Fronte</string> + <string name="camera_facing_back">Retro</string> + <string name="camera_facing_external">Esterno</string> + <string name="image_flip">Ruota Immagine</string> + + <!-- Graphics settings strings --> + <string name="vsync">Abilita V-Sync</string> + <string name="vsync_description">Sincronizza il frame rate dei giochi con il refresh rate del tuo dispositivo.</string> + <string name="linear_filtering">Abilita filtro lineare</string> + <string name="linear_filtering_description">Abilita il filtro lineare, che fa sembrare più smussata la grafica dei giochi.</string> + <string name="texture_filter_name">Filtro Texture</string> + <string name="texture_filter_description">Migliora la grafica dei giochi applicando un filtro alle texture. I filtri supportati sono Anime4k Ultrafast, Bicubic, ScaleForce, e xBRZ freescale.</string> + <string name="hw_renderer">Abilita renderer hardware</string> + <string name="hw_renderer_description">Utilizza l\'hardware per emulare la grafica del 3DS. Se abilitato, le prestazioni dei giochi miglioreranno significativamente.</string> + <string name="hw_shaders">Abilita shader hardware</string> + <string name="hw_shaders_description">Utilizza l\'hardware per emulare gli shader del 3DS. Se abilitato, le prestazioni dei giochi miglioreranno significativamente.</string> + <string name="shaders_accurate_mul">Abilita moltiplicazione shader accurata</string> + <string name="shaders_accurate_mul_description">Utilizza una moltiplicazione più accurata degli shader hardware, che potrebbe correggere alcuni bug grafici. Se abilitato, le prestazioni saranno ridotte.</string> + <string name="asynchronous_gpu">Abilita emulazione GPU asincrona</string> + <string name="asynchronous_gpu_description">Utilizza un thread separato per emulare la GPU in differita. Se abilitato, le prestazioni miglioreranno.</string> + <string name="frame_limit_enable">Abilita limite velocità</string> + <string name="frame_limit_enable_description">Se abilitato, la velocità d\'emulazione sarà limitata ad una percentuale specifica di velocità normale.</string> + <string name="frame_limit_slider">Percentuale limite velocità</string> + <string name="frame_limit_slider_description">Specifica a quale percentuale limitare la velocità d\'emulazione. Utilizzando l\'impostazione predefinita di 100% l\'emulazione sarà limitata alla velocità normale. Valori superiori o inferiori aumenteranno o diminuiranno il limite di velocità.</string> + <string name="internal_resolution">Risoluzione interna</string> + <string name="internal_resolution_description">Specifica a quale risoluzione renderizzare. Un\'alta risoluzione migliorerà di molto la qualità visiva ma risulta anche essere piuttosto pesante in termini di prestazioni e potrebbe causare glitch in alcuni giochi.</string> + <string name="performance_warning">Disabilitare questa impostazione riducerà significativamente le performance dell\'emulazione! Per un\'esperienza migliore, è consigliato lasciare questa impostazione abilitata.</string> + <string name="debug_warning">Attenzione: Modificare queste impostazioni rallenterà l\'emulazione</string> + + <!-- Premium strings --> + <string name="premium_text">Premium</string> + <string name="premium_settings_upsell">Passa a Premium e supporta Citra!</string> + <string name="premium_settings_upsell_description">Con la versione Premium, supporterai gli sviluppatori per continuare a migliorare Citra, e potrai accedere a queste funzionalità esclusive! </string> + <string name="premium_settings_welcome">Benvenuto in Premium.</string> + <string name="premium_settings_welcome_description">Grazie per il supporto!</string> + + <!-- Audio settings strings --> + <string name="audio_stretch">Abilita allungamento dell\'audio</string> + <string name="audio_stretch_description">Allunga l\'audio per ridurre lo stuttering. Se abilitato, aumenta la latenza dell\'audio e riduce lievemente le prestazioni.</string> + + <!-- Miscellaneous --> + <string name="clear">Ripristina</string> + <string name="slider_default">Default</string> + <string name="ini_saved">Impostazioni salvate</string> + <string name="gameid_saved">Impostazioni salvate per %1$s</string> + <string name="error_saving">Errore di salvataggio %1$s.ini: %2$s</string> + + <!-- Game Grid Screen--> + <string name="grid_menu_core_settings">Impostazioni</string> + + <!-- Add Directory Screen--> + <string name="select_game_folder">Seleziona Cartella di Gioco</string> + <string name="add_directory_title">Aggiungi una Cartella alla Libreria</string> + + <!-- Preferences Screen --> + <string name="preferences_settings">Impostazioni</string> + <string name="preferences_premium">Premium</string> + <string name="preferences_general">Generale</string> + <string name="preferences_system">Sistema</string> + <string name="preferences_camera">Fotocamera</string> + <string name="preferences_controls">Gamepad</string> + <string name="preferences_graphics">Grafica</string> + <string name="preferences_audio">Audio</string> + <string name="preferences_debug">Debug</string> + + <!-- ROM loading errors --> + <string name="loader_error_encrypted">La ROM è criptata</string> + <string name="loader_error_invalid_format">Formato ROM non valido</string> + + <!-- Emulation Menu --> + <string name="emulation_show_fps">Mostra FPS</string> + <string name="emulation_configure_controls">Configura Controlli</string> + <string name="emulation_edit_layout">Modifica Disposizione</string> + <string name="emulation_done">Fatto</string> + <string name="emulation_toggle_controls">Attiva Controlli</string> + <string name="emulation_control_scale">Regola la Dimensione</string> + <string name="emulation_open_settings">Apri Impostazioni</string> + <string name="emulation_switch_screen_layout">Disposizione Schermo Panorama</string> + <string name="emulation_screen_layout_landscape">Default</string> + <string name="emulation_screen_layout_portrait">Ritratto</string> + <string name="emulation_screen_layout_single">Schermo singolo</string> + <string name="emulation_screen_layout_sidebyside">Affiancati</string> + <string name="emulation_swap_screens">Inverti Schermi</string> + <string name="emulation_touch_overlay_reset">Ripristina Disposizione</string> + <string name="emulation_show_overlay">Mostra Disposizione</string> + <string name="emulation_close_game">Chiudi il Gioco</string> + <string name="emulation_close_game_message">Sei sicuro di voler chiudere il gioco corrente?</string> + <string name="menu_emulation_amiibo">Amiibo</string> + <string name="menu_emulation_amiibo_load">Carica</string> + <string name="menu_emulation_amiibo_remove">Rimuovi</string> + <string name="select_amiibo">Seleziona file Amiibo</string> + <string name="amiibo_load_error">Errore di caricamento Amiibo</string> + <string name="amiibo_load_error_message">Durante il caricamento di un file Amiibo, si è verificato un errore. Controlla che il file sia corretto.</string> + + <string name="write_permission_needed">L\'emulatore ha bisogno dei permessi per l\'archiviazione dei dati su memoria esterna per funzionare correttamente</string> + <string name="load_settings">Caricamento Impostazioni...</string> + + <string name="external_storage_not_mounted">C\'è bisogno di una memoria esterna disponibile per poter utilizzare Citra</string> + + <string name="select_dir">Seleziona Questa Cartella</string> + <string name="empty_gamelist">Non è stato trovato alcun file o non è ancora stata selezionata una cartella di gioco.</string> + + <!-- Software Keyboard --> + <string name="software_keyboard">Tastiera Software</string> + <string name="i_forgot">L\'ho Dimenticato</string> + <string name="fixed_length_required">La lunghezza del testo non è corretta (deve essere di %d caratteri)</string> + <string name="max_length_exceeded">Il testo è troppo lungo (non deve essere più lungo di %d caratteri)</string> + <string name="blank_input_not_allowed">Non è consentito lasciarlo vuoto</string> + <string name="empty_input_not_allowed">Non è consentito lasciarlo vuoto</string> + + <!-- Mii Selector --> + <string name="mii_selector">Selettore Mii</string> + <string name="standard_mii">Mii Standard</string> + + <!-- Camera --> + <string name="camera_select_image">Seleziona Immagine</string> + <string name="camera">Fotocamera</string> + <string name="camera_permission_needed">Citra ha bisogno dell\'accesso alla fotocamera per emulare quella del 3DS.\n\nIn alternativa, puoi impostare \"Immagine Sorgente\" su \"Immagine Fissa\" in Impostazioni Fotocamera.</string> +</resources> diff --git a/src/android/app/src/main/res/values-ja/strings.xml b/src/android/app/src/main/res/values-ja/strings.xml new file mode 100644 index 000000000..8b3a0e067 --- /dev/null +++ b/src/android/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_disclaimer">このソフトではニンテンドー3DS用ゲームをプレイできますが、ゲーム自体は含まれていません。\n\n実行前に、3DSゲームファイルを端末のストレージに保存しておいてください。</string> + <string name="app_notification_channel_description">Citra 3DS 通知</string> + <string name="app_notification_running">Citraを実行中</string> + + <!-- Input related strings --> + <string name="controller_circlepad">スライドパッド</string> + <string name="controller_c">Cスティック</string> + <string name="controller_triggers">トリガー</string> + <string name="controller_dpad">十字ボタン</string> + <string name="controller_axis_vertical">上下</string> + <string name="controller_axis_horizontal">左右</string> + <string name="input_binding">コントローラーの設定</string> + <string name="input_binding_description">ボタンorスティック入力で次のコントロールを設定: %1$s</string> + <string name="input_binding_description_vertical_axis">スティックを上か下に入力してください</string> + <string name="input_binding_description_horizontal_axis">スティックを左か右に入力してください</string> + <string name="input_message_analog_only">このコントロールはゲームパッドのスティック入力が必要です!</string> + <string name="input_message_button_only">このコントロールはゲームパッドのボタン入力が必要です!</string> + + <!-- Generic buttons (Shared with lots of stuff) --> + <string name="generic_buttons">ボタン</string> + + <!-- Core settings strings --> + <string name="cpu_jit">CPU JITを有効化</string> + <string name="cpu_jit_description">CPUのエミュレーションにジャストインタイム(JIT)コンパイラを使用します。有効にすると、パフォーマンスが大幅に向上します。</string> + <string name="init_clock">システム日時</string> + <string name="init_clock_description">エミュレートする3DSについて、デバイスの日時を反映させるか任意の日時を指定できます。</string> + + <!-- System settings strings --> + <string name="init_time">システムクロックで開始時間を上書き</string> + <string name="init_time_description">システム時刻を「任意の日時」に設定している場合、開始日時は固定されます。</string> + <string name="emulated_region">地域</string> + <string name="emulated_language">システム言語</string> + + <!-- Graphics settings strings --> + <string name="vsync">垂直同期(V-Sync)を有効化</string> + <string name="vsync_description">ゲームのフレームレートをデバイスのリフレッシュレートに同期させます。</string> + <string name="linear_filtering">リニアフィルタリングを有効化</string> + <string name="linear_filtering_description">有効にすると、よりなめらかな画質が期待できます。</string> + <string name="texture_filter_name">テクスチャフィルタ</string> + <string name="hw_renderer">ハードウェアレンダラを有効にする</string> + <string name="hw_renderer_description">グラフィックエミュレーションにハードウェアを使用します。有効にすると、パフォーマンスが大幅に向上します。</string> + <string name="hw_shaders">ハードウェアシェーダを有効にする</string> + <string name="hw_shaders_description">シェーダエミュレーションにハードウェアを使用します。有効にすると、パフォーマンスが大幅に向上します。</string> + <string name="shaders_accurate_mul">正確なシェーダ乗算を有効にする</string> + <string name="shaders_accurate_mul_description">ハードウェアシェーダ処理でより正確な乗算演算を行います。有効にするとグラフィックの問題が解消される可能性がありますが、パフォーマンスは低下します。</string> + <string name="frame_limit_enable">エミュレーション速度制限を有効化</string> + <string name="frame_limit_enable_description">有効にすると、エミュレーション速度が指定した値までに制限されます。</string> + <string name="frame_limit_slider">エミュレーション速度の指定</string> + <string name="frame_limit_slider_description">エミュレーション速度の割合を指定します。デフォルトは100%です。</string> + <string name="internal_resolution">内部解像度</string> + <string name="internal_resolution_description">レンダリング解像度を指定します。高い解像度は画質を大幅に向上させますが、パフォーマンスにも大きく影響し、特定のゲームで問題が発生する可能性があります。</string> + <string name="performance_warning">この設定をオフにすると、エミュレーション性能が大幅に低下します。最高のエクスペリエンスを実現するには、この設定を有効化にすることをお勧めします。</string> + <!-- Audio settings strings --> + <string name="audio_stretch">タイムストレッチを有効化</string> + <string name="audio_stretch_description">タイムストレッチ処理を行うことで音に関する問題を軽減させます。有効にすると音声遅延が増加し、パフォーマンスがわずかに低下します。</string> + + <!-- Miscellaneous --> + <string name="clear">クリア</string> + <string name="slider_default">デフォルト</string> + <string name="gameid_saved">設定を保存しました:%1$s</string> + <!-- Game Grid Screen--> + <string name="grid_menu_core_settings">設定</string> + + <!-- Add Directory Screen--> + <string name="select_game_folder">ゲームフォルダを選択</string> + <string name="add_directory_title">ライブラリにフォルダを追加</string> + + <!-- Preferences Screen --> + <string name="preferences_settings">設定</string> + <string name="preferences_general">一般</string> + <string name="preferences_system">システム</string> + <string name="preferences_controls">ゲームパッド</string> + <string name="preferences_graphics">グラフィック</string> + <string name="preferences_audio">サウンド</string> + <string name="preferences_debug">デバッグ</string> + + <!-- ROM loading errors --> + <string name="loader_error_encrypted">ROMが暗号化されています</string> + <string name="loader_error_invalid_format">無効なROMフォーマットです</string> + + <!-- Emulation Menu --> + <string name="emulation_show_fps">FPSを表示する</string> + <string name="emulation_configure_controls">コントロールの設定</string> + <string name="emulation_edit_layout">レイアウトを編集</string> + <string name="emulation_done">完了</string> + <string name="emulation_toggle_controls">コントロールの切り替え</string> + <string name="emulation_open_settings">設定を開く</string> + <string name="emulation_screen_layout_landscape">デフォルト</string> + <string name="emulation_screen_layout_single">Single Screen</string> + <string name="emulation_screen_layout_sidebyside">Side by Side Screens</string> + <string name="emulation_swap_screens">スクリーンの上下を入れ替える</string> + <string name="emulation_touch_overlay_reset">オーバーレイをリセット</string> + <string name="emulation_show_overlay">オーバーレイを表示</string> + <string name="emulation_close_game">ゲームを終了</string> + <string name="emulation_close_game_message">プレイ中のゲームを終了しますか?</string> + <string name="write_permission_needed">動作させるためには外部ストレージへのアクセスを許可する必要があります。</string> + <string name="load_settings">設定を読込中...</string> + + <string name="external_storage_not_mounted">Citraを利用するには外部ストレージが必要です。</string> + + <string name="select_dir">このフォルダを選択</string> + <string name="empty_gamelist">ゲームファイルのあるフォルダが選択されていません</string> + + <!-- Software Keyboard --> + <string name="software_keyboard">ソフトウェアキーボード</string> + <string name="fixed_length_required">テキストの長さが正しくありません (%d 文字以内にしてください)</string> + <string name="max_length_exceeded">テキストが長すぎます (%d 文字以内にしてください)</string> + <string name="blank_input_not_allowed">空白だけの入力はできません</string> + <string name="empty_input_not_allowed">入力なしのまま進めることはできません</string> + + </resources> diff --git a/src/android/app/src/main/res/values-ko/strings.xml b/src/android/app/src/main/res/values-ko/strings.xml new file mode 100644 index 000000000..9817cd022 --- /dev/null +++ b/src/android/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,169 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_disclaimer">이 소프트웨어는 닌텐도 3DS 휴대용 게임 콘솔을 실행합니다. 게임타이틀이 포함되어있지 않습니다.\n\n실행하기전에 합법적으로 소유한 3DS 게임을 기기저장소에 저장하십시오.</string> + <string name="app_notification_channel_description">CItra 3DS 에뮬레이터 알림</string> + <string name="app_notification_running">Citra 실행중</string> + + <!-- Input related strings --> + <string name="controller_circlepad">슬라이드 패드</string> + <string name="controller_c">C 스틱</string> + <string name="controller_triggers">트리거</string> + <string name="controller_dpad">십자키</string> + <string name="controller_axis_vertical">위/아래 Axis</string> + <string name="controller_axis_horizontal">왼쪽/오른쪽 Axis</string> + <string name="input_binding">입력 바인딩</string> + <string name="input_binding_description">입력을 누르거나 이동하여 %1$s에 바인딩합니다</string> + <string name="input_binding_description_vertical_axis">조이스틱을 위나 아래로 움직이세요.</string> + <string name="input_binding_description_horizontal_axis">조이스틱을 위나 좌우로 움직이세요.</string> + <string name="input_message_analog_only">이 컨트롤은 게임패드에 아날로그 스틱이나 십자패드 Asix에 바인딩 되어야 합니다!</string> + <string name="input_message_button_only">이 컨트롤은 게임패드 버튼에 바인딩 되어야 합니다!</string> + + <!-- Generic buttons (Shared with lots of stuff) --> + <string name="generic_buttons">버튼</string> + + <!-- Premium settings strings --> + <string name="design">테마변경 (Light, Dark)</string> + <string name="design_updated">설정을 종료하면 테마가 업데이트됩니다</string> + + <!-- Core settings strings --> + <string name="cpu_jit">CPU JIT 사용</string> + <string name="cpu_jit_description">CPU 에뮬레이션에 Just-in-Time(JIT) 컴파일러를 사용합니다. 활성화하면 게임 성능이 크게 향상됩니다.</string> + <string name="init_clock">시스템 시계 타입</string> + <string name="init_clock_description">에뮬레이트 된 3DS 클럭을 장치의 시간을 반영하거나 시뮬레이션 된 날짜 및 시간에 시작하도록 설정하십시오.</string> + + <!-- System settings strings --> + <string name="init_time">시스템 시계 시작시간 재정의</string> + <string name="init_time_description">\"System clock type\" 설정이 \"Simulated clock\"으로 설정되면 시작 날짜와 시간이 변경됩니다.</string> + <string name="emulated_region">에뮬레이션 지역</string> + <string name="emulated_language">에뮬레이션 언어</string> + + <!-- Camera settings strings --> + <string name="inner_camera">내부 카메라</string> + <string name="outer_left_camera">왼쪽 외부 카메라</string> + <string name="outer_right_camera">오른쪽 외부 카메라</string> + <string name="image_source">이미지 소스</string> + <string name="image_source_description">가상 카메라의 이미지 소스를 설정합니다. 이미지 파일 또는 지원되는 경우 장치 카메라를 사용할 수 있습니다.</string> + <string name="camera_device">카메라 장치</string> + <string name="camera_device_description">\"이미지 소스\" 설정이 \"장치 카메라\"로 설정된 경우 실제 카메라로 설정됩니다.</string> + <string name="camera_facing_front">전면</string> + <string name="camera_facing_back">후면</string> + <string name="camera_facing_external">외부</string> + <string name="image_flip">이미지 뒤집기</string> + + <!-- Graphics settings strings --> + <string name="vsync">V-Sync 사용</string> + <string name="vsync_description">게임 프레임 속도를 장치의 재생 빈도와 동기화합니다.</string> + <string name="linear_filtering">리니어 필터링 사용</string> + <string name="linear_filtering_description">게임 필터링이 매끄럽게 보이도록 선형 필터링을 활성화합니다.</string> + <string name="texture_filter_name">텍스처 필터</string> + <string name="texture_filter_description">텍스처에 필터를 적용하여 게임의 시각적 효과를 향상시킵니다. 지원되는 필터는 Anime4K Ultrafast, Bicubic, ScaleForce 및 xBRZ 프리스케일입니다.</string> + <string name="hw_renderer">하드웨어 렌더러 사용</string> + <string name="hw_renderer_description">하드웨어를 사용하여 3DS 그래픽을 에뮬레이션합니다. 활성화하면 게임 성능이 크게 향상됩니다.</string> + <string name="hw_shaders">하드웨어 쉐이더 사용</string> + <string name="hw_shaders_description">하드웨어를 사용하여 3DS 쉐이더를 에뮬레이션합니다. 활성화하면 게임 성능이 크게 향상됩니다.</string> + <string name="shaders_accurate_mul">정확한 쉐이더 곱셉 사용</string> + <string name="shaders_accurate_mul_description">하드웨어 쉐이더에서 더 정확한 곱셈을 사용합니다. 일부 그래픽 버그가 수정 될 수 있습니다. 활성화하면 성능이 저하됩니다.</string> + <string name="asynchronous_gpu">비동기 GPU 에뮬레이션 사용</string> + <string name="asynchronous_gpu_description">별도의 스레드를 사용하여 GPU를 비동기적으로 에뮬레이트합니다. 활성화하면 성능이 향상됩니다.</string> + <string name="frame_limit_enable">속도 제한 사용</string> + <string name="frame_limit_enable_description">활성화하면 에뮬레이션 속도가 정상속도의 지정된 비율로 제한됩니다.</string> + <string name="frame_limit_slider">속도 퍼센트 제한</string> + <string name="frame_limit_slider_description">에뮬레이션 속도를 제한할 퍼센트을 지정합니다. 기본값인 100%을 사용하면 에뮬레이션은 정상 속도로 제한됩니다. 더 높거나 낮은 값은 제한 속도를 늘리거나 줄입니다.</string> + <string name="internal_resolution">내부 해상도</string> + <string name="internal_resolution_description">렌더링에 사용되는 해상도를 지정합니다. 해상도가 높으면 시각적 품질이 많이 향상되지만 성능이 상당히 높아 특정 게임에서 글리치가 발생할 수 있습니다.</string> + <string name="performance_warning">이 설정을 끄면 에뮬레이션 성능이 크게 저하됩니다! 최상의 경험을 위해서는 이 설정을 사용하는 것이 좋습니다.</string> + <string name="debug_warning">경고: 이 설정을 수정하면 에뮬레이션 속도가 느려집니다</string> + + <!-- Premium strings --> + <string name="premium_text">프리미엄</string> + <string name="premium_settings_upsell">프리미엄으로 업그레이드하고 Citra를 지원하십시오!</string> + <string name="premium_settings_upsell_description">프리미엄으로 개발자가 Citra를 계속 개선 할 수 있도록 지원하고 이 독점 기능에 액세스하세요!</string> + <string name="premium_settings_welcome">프리미엄에 오신 것을 환영합니다.</string> + <string name="premium_settings_welcome_description">지원해주셔서 감사합니다!</string> + + <!-- Audio settings strings --> + <string name="audio_stretch">오디오 스트레칭 사용</string> + <string name="audio_stretch_description">Stretches audio to reduce stuttering. When enabled, increases audio latency and slightly reduces performance. + +오디오 스터터링을 줄이기 위해 오디오를 늘립니다. 활성화하면 오디오 레이턴시가 증가하고 성능이 약간 저하됩니다.</string> + + <!-- Miscellaneous --> + <string name="clear">지우기</string> + <string name="slider_default">기본</string> + <string name="ini_saved">저장된 설정</string> + <string name="gameid_saved">%1$s의 저장된 설정</string> + <string name="error_saving">%1$s.ini 저장 오류: %2$s </string> + + <!-- Game Grid Screen--> + <string name="grid_menu_core_settings">설정</string> + + <!-- Add Directory Screen--> + <string name="select_game_folder">게임 폴더 선택</string> + <string name="add_directory_title">폴더를 라이브러리에 추가</string> + + <!-- Preferences Screen --> + <string name="preferences_settings">설정</string> + <string name="preferences_premium">프리미엄</string> + <string name="preferences_general">일반</string> + <string name="preferences_system">설정</string> + <string name="preferences_camera">카메라</string> + <string name="preferences_controls">게임패드</string> + <string name="preferences_graphics">그래픽</string> + <string name="preferences_audio">오디오</string> + <string name="preferences_debug">디버그</string> + + <!-- ROM loading errors --> + <string name="loader_error_encrypted">롬이 암호화되어 있습니다</string> + <string name="loader_error_invalid_format">올바르지 않은 롬 포맷</string> + + <!-- Emulation Menu --> + <string name="emulation_show_fps">FPS 보이기</string> + <string name="emulation_configure_controls">컨트롤 설정</string> + <string name="emulation_edit_layout">레이아웃 편집</string> + <string name="emulation_done">완료</string> + <string name="emulation_toggle_controls">컨트롤 토글</string> + <string name="emulation_control_scale">스케일 조정</string> + <string name="emulation_open_settings">설정 열기</string> + <string name="emulation_switch_screen_layout">가로 화면 레이아웃</string> + <string name="emulation_screen_layout_landscape">기본</string> + <string name="emulation_screen_layout_portrait">세로</string> + <string name="emulation_screen_layout_single">단일화면</string> + <string name="emulation_screen_layout_sidebyside">좌우화면 (Side by Side) 스크린</string> + <string name="emulation_swap_screens">스크린 바꾸기</string> + <string name="emulation_touch_overlay_reset">오버레이 리셋</string> + <string name="emulation_show_overlay">오버레이 보이기</string> + <string name="emulation_close_game">게임 닫기</string> + <string name="emulation_close_game_message">현재 게임을 닫으시겠습니까?</string> + <string name="menu_emulation_amiibo">아미보</string> + <string name="menu_emulation_amiibo_load">열기</string> + <string name="menu_emulation_amiibo_remove">제거</string> + <string name="select_amiibo">아미보 파일 선택</string> + <string name="amiibo_load_error">아미보 열기 오류</string> + <string name="amiibo_load_error_message">지정된 아미보 파일을 여는 중에 오류가 발생했습니다. 파일이 올바른지 확인하세요.</string> + + <string name="write_permission_needed">에뮬레이터가 작동하려면 외부 저장소에 대한 쓰기 액세스를 허용해야합니다.</string> + <string name="load_settings"> 설정 불러오는중...</string> + + <string name="external_storage_not_mounted">Citra를 사용하려면 외부 저장소를 사용할 수 있어야합니다</string> + + <string name="select_dir">이 디렉토리 선택</string> + <string name="empty_gamelist">파일이 없거나 아직 게임 디렉토리가 선택되지 않았습니다.</string> + + <!-- Software Keyboard --> + <string name="software_keyboard">소프트웨어 키보드</string> + <string name="i_forgot">잊어버렸습니다</string> + <string name="fixed_length_required">텍스트 길이가 올바르지 않습니다 (%d자여야 합니다)</string> + <string name="max_length_exceeded">텍스트가 너무 깁니다 (%d 이하여야 합니다 )</string> + <string name="blank_input_not_allowed">공백 입력은 허용되지 않습니다.</string> + <string name="empty_input_not_allowed">빈 입력은 허용되지 않습니다.</string> + + <!-- Mii Selector --> + <string name="mii_selector">Mii 선택기</string> + <string name="standard_mii">기본 Mii</string> + + <!-- Camera --> + <string name="camera_select_image">이미지 선택</string> + <string name="camera">카메라</string> + <string name="camera_permission_needed">Citra는 3DS의 카메라를 에뮬레이션하기 위해 카메라에 액세스해야합니다.\n\n 그 대신에 카메라 설정에서 \"이미지 소스\"를 \"정지 이미지\"로 설정할 수도 있습니다.</string> +</resources> diff --git a/src/android/app/src/main/res/values-nb/strings.xml b/src/android/app/src/main/res/values-nb/strings.xml new file mode 100644 index 000000000..d8314c8b5 --- /dev/null +++ b/src/android/app/src/main/res/values-nb/strings.xml @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_disclaimer">Denne programvaren kjører spill for Nintendo 3DS håndholdte spillkonsoll. Ingen spilltitler er inkludert.\n\nFør du starter, vennligst plasser dine rettmessig eide 3DS-spillfiler på lagringsenheten.</string> + <string name="app_notification_channel_description">Citra 3DS emulator notifikasjoner</string> + <string name="app_notification_running">Citra kjører</string> + + <!-- Input related strings --> + <string name="controller_circlepad">Gliplate</string> + <string name="controller_c">C-Spak</string> + <string name="controller_triggers">Utløser</string> + <string name="controller_dpad">Kontrollpluss</string> + <string name="controller_axis_vertical">Opp/Ned Akse</string> + <string name="controller_axis_horizontal">Venstre/Høyre Akse</string> + <string name="input_binding">Inngangsinnbinding</string> + <string name="input_binding_description">Trykk eller flytt en inngang for å binde den til %1$s.</string> + <string name="input_binding_description_vertical_axis">Beveg Joystick opp eller ned</string> + <string name="input_binding_description_horizontal_axis">Beveg Joystick venstre eller høyre</string> + <string name="input_message_analog_only">Denne kontrollen må være bundet til en håndkontroller\'s analog spak eller kontrollpluss akse!</string> + <string name="input_message_button_only">Denne kontrollen må være bundet til en håndkontroller knapp!</string> + + <!-- Generic buttons (Shared with lots of stuff) --> + <string name="generic_buttons">Taster</string> + + <!-- Premium settings strings --> + <string name="design">Endre Tema (Lys, Mørk)</string> + <string name="design_updated">Tema oppdateres når du går ut av Innstillinger</string> + + <!-- Core settings strings --> + <string name="cpu_jit">Aktiver CPU JIT</string> + <string name="cpu_jit_description">Bruker Just-in-Time (JIT) kompilator til CPU emulering. Når dette er aktivert, vil spill ytelsen bli betydelig forbedret.</string> + <string name="init_clock">Systemklokketype</string> + <string name="init_clock_description">Sett den emulerte 3DS-klokken til enten å gjenspeile den på enheten din, eller start på en simulert dato og tid.</string> + + <!-- System settings strings --> + <string name="init_time">Overstyring av starttid på systemklokken</string> + <string name="init_time_description">Hvis \"Systemklokketypen\" innstillingen er satt til \"Simulert Klokke\" Endrer dette startpunktet til den datoen og klokkeslettet.</string> + <string name="emulated_region">Emulert region</string> + <string name="emulated_language">Emulert Språk</string> + + <!-- Camera settings strings --> + <string name="inner_camera">Indre Kamera</string> + <string name="outer_left_camera">Ytre Venstre Kamera</string> + <string name="outer_right_camera">Ytre Høyre Kamera</string> + <string name="image_source">Bildekilde</string> + <string name="image_source_description">Angir bildekilden til det virtuelle kameraet. Du kan bruke en bildefil, eller en kamera enhet når det er støttet.</string> + <string name="camera_device">Kamera Enhet</string> + <string name="camera_device_description">Hvis innstillingen \"Bildekilde\" er satt til \"Kamera Enhet\", angir dette det fysiske kameraet som skal brukes.</string> + <string name="camera_facing_front">Foran</string> + <string name="camera_facing_back">Bak</string> + <string name="camera_facing_external">Ekstern</string> + <string name="image_flip">Vend Bilde</string> + + <!-- Graphics settings strings --> + <string name="vsync">Aktiver V-Sync</string> + <string name="vsync_description">Synkroniserer spillrammefrekvensen med oppdateringsfrekvensen på enheten din.</string> + <string name="linear_filtering">Aktiver lineær filtrering</string> + <string name="linear_filtering_description">Aktiverer lineær filtrering, noe som får spillvisualer til å vises jevnere.</string> + <string name="texture_filter_name">Tekstur Filter</string> + <string name="texture_filter_description">Forbedrer det visuelle i spill ved å bruke et filter på teksturer. De støttede filtrene er Anime4K Ultrafast, Bicubic, ScaleForce og xBRZ freescale.</string> + <string name="hw_renderer">Aktiver maskinvaregjengivelse</string> + <string name="hw_renderer_description">Bruker maskinvare til å emulere 3DS grafikk. Når dette er aktivert, vil spillytelsen bli betydelig forbedret.</string> + <string name="hw_shaders">Aktiver maskinvare shader</string> + <string name="hw_shaders_description">Bruker maskinvare for å etterligne 3DS shaders. Når dette er aktivert, vil spillytelsen bli betydelig forbedret.</string> + <string name="shaders_accurate_mul">Aktiver nøyaktig shader-multiplikasjon</string> + <string name="shaders_accurate_mul_description">Bruker mer nøyaktig multiplikasjon i maskinvare shaders, som kan fikse noen grafiske feil. Når dette er aktivert, reduseres ytelsen.</string> + <string name="asynchronous_gpu">Aktiver asynkron GPU-emulering</string> + <string name="asynchronous_gpu_description">Bruker en egen tråd for å emulere GPU asynkront. Når dette er aktivert, forbedres ytelsen.</string> + <string name="frame_limit_enable">Aktiver fartsbegrensning</string> + <string name="frame_limit_enable_description">Når dette er aktivert, vil emuleringshastigheten være begrenset til en spesifisert prosentandel av normal hastighet.</string> + <string name="frame_limit_slider">Begrens fartsprosent </string> + <string name="frame_limit_slider_description">Angir prosentandelen som skal begrense emuleringshastighet. Med standard 100% emulering vil være begrenset til normal hastighet. Verdier høyere eller lavere vil øke eller redusere fartsgrensen.</string> + <string name="internal_resolution">Intern oppløsning</string> + <string name="internal_resolution_description">Angir oppløsningen som brukes til å gjengis på. En høy oppløsning vil forbedre visuell kvalitet mye, men er også ganske tung på ytelsen og kan forårsake feil i visse spill.</string> + <string name="performance_warning">Å slå av denne innstillingen vil redusere emuleringsytelsen betydelig! For den beste opplevelsen, anbefales det at du lar denne innstillingen være aktivert.</string> + <string name="debug_warning">Advarsel: Endring av disse innstillingene reduserer emuleringen</string> + + <!-- Premium strings --> + <string name="premium_text">Premium</string> + <string name="premium_settings_upsell">Oppgrader til Premium og støtt Citra!</string> + <string name="premium_settings_upsell_description">Med Premium vil du støtte utviklerne til å fortsette å forbedre Citra, og få tilgang til disse eksklusive funksjonene!</string> + <string name="premium_settings_welcome">Velkommen til Premium.</string> + <string name="premium_settings_welcome_description">Takk for støtten!</string> + + <!-- Audio settings strings --> + <string name="audio_stretch">Aktiver lydstrekking</string> + <string name="audio_stretch_description">Strekker lyd for å redusere stamming. Når dette er aktivert, øker lydforsinkelsen og reduserer ytelsen litt.</string> + + <!-- Miscellaneous --> + <string name="clear">Tøm</string> + <string name="slider_default">Standard</string> + <string name="ini_saved">Lagret Innstillingene</string> + <string name="gameid_saved">Lagret innstillinger for %1$s</string> + <string name="error_saving">Feil ved lagring av %1$s.ini: %2$s</string> + + <!-- Game Grid Screen--> + <string name="grid_menu_core_settings">Innstillinger</string> + + <!-- Add Directory Screen--> + <string name="select_game_folder">Velg Spill Mappe</string> + <string name="add_directory_title">Lett til Mappe til Bibliotek </string> + + <!-- Preferences Screen --> + <string name="preferences_settings">Innstillinger </string> + <string name="preferences_premium">Premium</string> + <string name="preferences_general">Generelt</string> + <string name="preferences_system">System</string> + <string name="preferences_camera">Kamera</string> + <string name="preferences_controls">Håndkontroller</string> + <string name="preferences_graphics">Grafikk</string> + <string name="preferences_audio">Lyd</string> + <string name="preferences_debug">Feilsøk</string> + + <!-- ROM loading errors --> + <string name="loader_error_encrypted">Ditt ROM er kryptert</string> + <string name="loader_error_invalid_format">Ugyldig ROM format </string> + + <!-- Emulation Menu --> + <string name="emulation_show_fps">Vis FPS</string> + <string name="emulation_configure_controls">Konfigurer Kontroller</string> + <string name="emulation_edit_layout">Endre Utseende </string> + <string name="emulation_done">Ferdig</string> + <string name="emulation_toggle_controls">Veksle Kontroller</string> + <string name="emulation_control_scale">Juster Skala</string> + <string name="emulation_open_settings">Åpne Innstillinger</string> + <string name="emulation_switch_screen_layout">Landskap Skjermoppsett</string> + <string name="emulation_screen_layout_landscape">Standard</string> + <string name="emulation_screen_layout_portrait">Bilde</string> + <string name="emulation_screen_layout_single">Enkel Skjerm</string> + <string name="emulation_screen_layout_sidebyside">Side ved Side Skjermer</string> + <string name="emulation_swap_screens">Bytt Skjerm</string> + <string name="emulation_touch_overlay_reset">Tilbakestill Overlegg</string> + <string name="emulation_show_overlay">Vis Overlegg</string> + <string name="emulation_close_game">Lukk Spill</string> + <string name="emulation_close_game_message">Er du sikker på at du vil lukke det nåværende spillet?</string> + <string name="menu_emulation_amiibo">Amiibo</string> + <string name="menu_emulation_amiibo_load">Last inn</string> + <string name="menu_emulation_amiibo_remove">Fjern</string> + <string name="select_amiibo">Velg Amiibo Fil</string> + <string name="amiibo_load_error">Feil ved lesing av Amiibo</string> + <string name="amiibo_load_error_message">Under lasting av den angitte Amiibo-filen oppstod det en feil. Kontroller at filen er riktig.</string> + + <string name="write_permission_needed">Du må gi skrivetilgang til ekstern lagring for at emulatoren skal fungere</string> + <string name="load_settings">Laster Innstillinger...</string> + + <string name="external_storage_not_mounted">Den eksterne lagringen må være tilgjengelig for å kunne bruke Citra</string> + + <string name="select_dir">Velg Denne Mappen</string> + <string name="empty_gamelist">Ingen filer ble funnet, eller det er ikke valgt noen spillkatalog ennå.</string> + + <!-- Software Keyboard --> + <string name="software_keyboard">Programvare Tastatur</string> + <string name="i_forgot">Jeg har Glemt</string> + <string name="fixed_length_required">Tekstlengden er ikke korrekt (må være %d tegn)</string> + <string name="max_length_exceeded">teksten er for lang (må ikke være mer enn %d tegn)</string> + <string name="blank_input_not_allowed">Blank felt er ikke tillatt</string> + <string name="empty_input_not_allowed">Tomt felt er ikke tillatt</string> + + <!-- Mii Selector --> + <string name="mii_selector">Mii Velger</string> + <string name="standard_mii">Standard Mii</string> + + <!-- Camera --> + <string name="camera_select_image">Velg Bilde</string> + <string name="camera">Kamera</string> + <string name="camera_permission_needed">Citra trenger tilgang til kameraet ditt for å emulere 3DS-kameraene.\n\n Alternativt kan du også stille \"Bildekilde\" til \"Stillbilde\" i kamerainnstillinger.</string> +</resources> diff --git a/src/android/app/src/main/res/values-night/colors.xml b/src/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 000000000..43b948021 --- /dev/null +++ b/src/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <color name="citra_orange">#272727</color> + <color name="citra_orange_dark">#121212</color> + <color name="citra_accent">#FEC303</color> + + <color name="card_view_background">#121212</color> + <color name="card_view_disabled">#3D3D3D</color> + + <color name="gamelist_divider">#404040</color> + + <color name="header_text">#E0E0E0</color> + <color name="header_subtext">#A0A0A0</color> + + <color name="citra_logo_text_color">@color/citra_accent</color> +</resources> diff --git a/src/android/app/src/main/res/values-night/styles_filepicker.xml b/src/android/app/src/main/res/values-night/styles_filepicker.xml new file mode 100644 index 000000000..1a175cdcf --- /dev/null +++ b/src/android/app/src/main/res/values-night/styles_filepicker.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="FilePickerBaseTheme" parent="NNF_BaseTheme" /> +</resources> diff --git a/src/android/app/src/main/res/values-pt/strings.xml b/src/android/app/src/main/res/values-pt/strings.xml new file mode 100644 index 000000000..73af7d039 --- /dev/null +++ b/src/android/app/src/main/res/values-pt/strings.xml @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_disclaimer">Este software emulará jogos do console portátil Nintendo 3DS. Nenhum jogo está incluso.\n\nAntes de abri-lo, coloque os arquivos dos jogos de 3DS que você possui no armazenamento do dispositivo.</string> + <string name="app_notification_channel_description">Notificações do emulador de 3DS Citra</string> + <string name="app_notification_running">O Citra está em execução</string> + + <!-- Input related strings --> + <string name="controller_circlepad">Analógico</string> + <string name="controller_c">Direcional C-Stick</string> + <string name="controller_triggers">Gatilhos</string> + <string name="controller_dpad">Direcional D-Pad</string> + <string name="controller_axis_vertical">Eixo vertical</string> + <string name="controller_axis_horizontal">Eixo horizontal</string> + <string name="input_binding">Mapeamento de controles</string> + <string name="input_binding_description">Pressione ou mova um botão/alavanca para mapear para %1$s.</string> + <string name="input_binding_description_vertical_axis">Mova o seu joystick para cima ou para baixo.</string> + <string name="input_binding_description_horizontal_axis">Mova o seu joystick para esquerda ou para direita.</string> + <string name="input_message_analog_only">Este controle precisa ser mapeado para um eixo analógico do gamepad ou um eixo de D-pad!</string> + <string name="input_message_button_only">Este controle deve ser mapeado para um botão do gamepad!</string> + + <!-- Generic buttons (Shared with lots of stuff) --> + <string name="generic_buttons">Botões</string> + + <!-- Premium settings strings --> + <string name="design">Alterar tema (claro, escuro)</string> + <string name="design_updated">O tema será atualizado ao sair das configurações</string> + + <!-- Core settings strings --> + <string name="cpu_jit">Ativar CPU JIT</string> + <string name="cpu_jit_description">Utiliza um compilador Just-in-Time (JIT) para emulação da CPU. Quando ativado, o desempenho do jogo será consideravelmente melhorado.</string> + <string name="init_clock">Tipo do relógio do sistema</string> + <string name="init_clock_description">Configura o relógio emulado do 3DS para refletir o do seu dispositivo ou para começar numa data e hora simulados.</string> + + <!-- System settings strings --> + <string name="init_time">Horário inicial do relógio do sistema</string> + <string name="init_time_description">Se a configuração \"Tipo de relógio do sistema\" estiver como \"Relógio simulado\", esta opção define a data e hora do sistema no início da emulação.</string> + <string name="emulated_region">Região emulada</string> + <string name="emulated_language">Idioma emulado</string> + + <!-- Camera settings strings --> + <string name="inner_camera">Câmera frontal</string> + <string name="outer_left_camera">Câmera traseira esquerda</string> + <string name="outer_right_camera">Câmera traseira direita</string> + <string name="image_source">Origem da imagem</string> + <string name="image_source_description">Define a origem da imagem da câmera virtual. Você pode usar um arquivo de imagem ou uma câmera do dispositivo, quando compatível.</string> + <string name="camera_device">Câmera do dispositivo</string> + <string name="camera_device_description">Se a \"Origem da imagem\" for definida como \"Câmera do dispositivo\", esta opção define a câmera física a ser usada.</string> + <string name="camera_facing_front">Frontal</string> + <string name="camera_facing_back">Traseira</string> + <string name="camera_facing_external">Externa</string> + <string name="image_flip">Espelhar imagem</string> + + <!-- Graphics settings strings --> + <string name="vsync">Ativar V-Sync</string> + <string name="vsync_description">Sincroniza a taxa de quadros do jogo com a taxa de atualização da tela do seu dispositivo.</string> + <string name="linear_filtering">Ativar filtragem linear</string> + <string name="linear_filtering_description">Ativa a filtragem linear, que suaviza o visual do jogo.</string> + <string name="texture_filter_name">Filtro de texturas</string> + <string name="texture_filter_description">Aprimora o visual dos jogos ao aplicar filtros às texturas. Os filtros compatíveis são: Anime4K Ultrafast, Bicúbico, ScaleForce e xBRZ Freescale.</string> + <string name="hw_renderer">Ativar renderizador por hardware</string> + <string name="hw_renderer_description">Utiliza o hardware para emular os gráficos do 3DS. Quando ativado, o desempenho de jogo será consideravelmente melhorado.</string> + <string name="hw_shaders">Ativar shaders via hardware</string> + <string name="hw_shaders_description">Utiliza o hardware para emular os shaders do 3DS. Quando ativado, o desempenho do jogo será consideravelmente melhorado.</string> + <string name="shaders_accurate_mul">Ativar multiplicação precisa de shaders</string> + <string name="shaders_accurate_mul_description">Utiliza uma multiplicação mais precisa de shaders no hardware, o que pode corrigir problemas visuais. Quando ativada, pode haver redução no desempenho.</string> + <string name="asynchronous_gpu">Ativar emulação de GPU assíncrona</string> + <string name="asynchronous_gpu_description">Usa uma thread separada para emular a GPU de forma assíncrona. Esta opção aprimora o desempenho quando ativada.</string> + <string name="frame_limit_enable">Ativar limite de velocidade</string> + <string name="frame_limit_enable_description">Quando ativado, a velocidade da emulação será limitada a uma porcentagem da velocidade normal.</string> + <string name="frame_limit_slider">Porcentagem de limitação da velocidade</string> + <string name="frame_limit_slider_description">Especifica a porcentagem para limitar a velocidade. Com o padrão de 100% a emulação será limitada a velocidade normal. Valores maiores ou menores vão aumentar ou reduzir o limite de velocidade.</string> + <string name="internal_resolution">Resolução interna</string> + <string name="internal_resolution_description">Especifica a resolução utilizada para renderização. Uma resolução alta melhorará muito a qualidade visual, mas poderá também impactar muito no desempenho e causar problemas em certos jogos.</string> + <string name="performance_warning">Desativar esta opção reduzirá consideravelmente o desempenho da emulação! Para obter a melhor experiência, recomendamos deixá-la ativada.</string> + <string name="debug_warning">Aviso: modificar estas configurações tornará a emulação mais lenta</string> + + <!-- Premium strings --> + <string name="premium_text">Premium</string> + <string name="premium_settings_upsell">Passe para o plano Premium e ajude o Citra!</string> + <string name="premium_settings_upsell_description">Com o Premium, você dará o seu apoio para que os desenvolvedores continuem a melhorar o Citra e também ganhará acesso a recursos exclusivos!</string> + <string name="premium_settings_welcome">Bem-vindo ao Premium.</string> + <string name="premium_settings_welcome_description">Agradecemos pelo seu apoio!</string> + + <!-- Audio settings strings --> + <string name="audio_stretch">Ativar alongamento de áudio</string> + <string name="audio_stretch_description">Estica o áudio para reduzir engasgos. Quando ativado, aumenta a latência do áudio e reduz levemente o desempenho.</string> + + <!-- Miscellaneous --> + <string name="clear">Limpar</string> + <string name="slider_default">Padrão</string> + <string name="ini_saved">As configurações foram salvas</string> + <string name="gameid_saved">As configurações de %1$s foram salvas</string> + <string name="error_saving">Erro ao salvar %1$s.ini: %2$s</string> + + <!-- Game Grid Screen--> + <string name="grid_menu_core_settings">Configurações</string> + + <!-- Add Directory Screen--> + <string name="select_game_folder">Escolher pasta de jogos</string> + <string name="add_directory_title">Adicionar pasta à biblioteca</string> + + <!-- Preferences Screen --> + <string name="preferences_settings">Configurações</string> + <string name="preferences_premium">Premium</string> + <string name="preferences_general">Geral</string> + <string name="preferences_system">Sistema</string> + <string name="preferences_camera">Câmera</string> + <string name="preferences_controls">Controle</string> + <string name="preferences_graphics">Gráficos</string> + <string name="preferences_audio">Áudio</string> + <string name="preferences_debug">Depuração</string> + + <!-- ROM loading errors --> + <string name="loader_error_encrypted">A sua ROM está criptografada</string> + <string name="loader_error_invalid_format">Formato inválido de ROM</string> + + <!-- Emulation Menu --> + <string name="emulation_show_fps">Mostrar FPS</string> + <string name="emulation_configure_controls">Configurar controles</string> + <string name="emulation_edit_layout">Editar esquema</string> + <string name="emulation_done">Pronto</string> + <string name="emulation_toggle_controls">Alternar controles</string> + <string name="emulation_control_scale">Ajustar escala</string> + <string name="emulation_open_settings">Abrir configurações</string> + <string name="emulation_switch_screen_layout">Layout de tela em paisagem</string> + <string name="emulation_screen_layout_landscape">Padrão</string> + <string name="emulation_screen_layout_portrait">Retrato</string> + <string name="emulation_screen_layout_single">Tela única</string> + <string name="emulation_screen_layout_sidebyside">Telas lado a lado</string> + <string name="emulation_swap_screens">Trocar telas</string> + <string name="emulation_touch_overlay_reset">Redefinir sobreposição</string> + <string name="emulation_show_overlay">Exibir sobreposição</string> + <string name="emulation_close_game">Fechar jogo</string> + <string name="emulation_close_game_message">Deseja mesmo fechar o jogo atual?</string> + <string name="menu_emulation_amiibo">Amiibo</string> + <string name="menu_emulation_amiibo_load">Carregar</string> + <string name="menu_emulation_amiibo_remove">Remover</string> + <string name="select_amiibo">Selecionar arquivo de Amiibo</string> + <string name="amiibo_load_error">Erro ao carregar Amiibo</string> + <string name="amiibo_load_error_message">Ocorreu um erro ao carregar o arquivo de Amiibo selecionado. Verifique se é o arquivo correto.</string> + + <string name="write_permission_needed">Você precisa permitir o acesso de escrita no armazenamento externo para que o emulador funcione</string> + <string name="load_settings">Carregando configurações...</string> + + <string name="external_storage_not_mounted">O armazenamento externo precisa estar disponível para que o Citra seja utilizado.</string> + + <string name="select_dir">Selecionar esta pasta</string> + <string name="empty_gamelist">Nenhum arquivo foi encontrado ou nenhuma pasta de jogos foi selecionada ainda.</string> + + <!-- Software Keyboard --> + <string name="software_keyboard">Teclado virtual</string> + <string name="i_forgot">Esqueci</string> + <string name="fixed_length_required">O tamanho do texto não está correto (precisa ter %dcaracteres)</string> + <string name="max_length_exceeded">O texto é muito longo (não deve ter mais que %d caracteres)</string> + <string name="blank_input_not_allowed">Controle vazio não é permitido</string> + <string name="empty_input_not_allowed">Controle vazio não é permitido</string> + + <!-- Mii Selector --> + <string name="mii_selector">Seletor de Mii</string> + <string name="standard_mii">Mii padrão</string> + + <!-- Camera --> + <string name="camera_select_image">Selecionar imagem</string> + <string name="camera">Câmera</string> + <string name="camera_permission_needed">O Citra precisa de acesso à sua câmera para emular as câmeras do 3DS.\n\nVocê também tem a opção de definir a \"Origem da imagem\" como \"Imagem estática\" nas configurações de câmera.</string> +</resources> diff --git a/src/android/app/src/main/res/values-w1000dp/integers.xml b/src/android/app/src/main/res/values-w1000dp/integers.xml new file mode 100644 index 000000000..5cd4e24f3 --- /dev/null +++ b/src/android/app/src/main/res/values-w1000dp/integers.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <integer name="game_grid_columns">4</integer> +</resources> \ No newline at end of file diff --git a/src/android/app/src/main/res/values-w1050dp/dimens.xml b/src/android/app/src/main/res/values-w1050dp/dimens.xml new file mode 100644 index 000000000..92fcb2b66 --- /dev/null +++ b/src/android/app/src/main/res/values-w1050dp/dimens.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Example customization of dimensions originally defined in res/values/dimens.xml + (such as screen margins) for screens with more than 1024dp of available width. --> + <dimen name="activity_horizontal_margin">96dp</dimen> +</resources> diff --git a/src/android/app/src/main/res/values-w500dp/integers.xml b/src/android/app/src/main/res/values-w500dp/integers.xml new file mode 100644 index 000000000..d2955c0ae --- /dev/null +++ b/src/android/app/src/main/res/values-w500dp/integers.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <integer name="game_grid_columns">2</integer> +</resources> \ No newline at end of file diff --git a/src/android/app/src/main/res/values-w750dp/integers.xml b/src/android/app/src/main/res/values-w750dp/integers.xml new file mode 100644 index 000000000..f049d8b44 --- /dev/null +++ b/src/android/app/src/main/res/values-w750dp/integers.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <integer name="game_grid_columns">3</integer> +</resources> \ No newline at end of file diff --git a/src/android/app/src/main/res/values-w820dp/dimens.xml b/src/android/app/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 000000000..d27181e85 --- /dev/null +++ b/src/android/app/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,5 @@ +<resources> + <!-- Example customization of dimensions originally defined in res/values/dimens.xml + (such as screen margins) for screens with more than 820dp of available width. --> + <dimen name="activity_horizontal_margin">64dp</dimen> +</resources> diff --git a/src/android/app/src/main/res/values-zh/strings.xml b/src/android/app/src/main/res/values-zh/strings.xml new file mode 100644 index 000000000..424d7cb8f --- /dev/null +++ b/src/android/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_disclaimer">此软件可以运行任天堂 3DS 掌上游戏机的游戏。本软件不包括任何任天堂 3DS 游戏。\n\n在运行它之前,请将您合法取得的 3DS 游戏文件放置于设备存储中。</string> + <string name="app_notification_channel_description">Citra 3DS 模拟器</string> + <string name="app_notification_running">Citra 正在运行</string> + + <!-- Input related strings --> + <string name="controller_circlepad">方向摇杆</string> + <string name="controller_c">C 摇杆</string> + <string name="controller_triggers">双肩按键</string> + <string name="controller_dpad">十字方向键</string> + <string name="controller_axis_vertical">上/下轴</string> + <string name="controller_axis_horizontal">左/右轴</string> + <string name="input_binding">绑定输入</string> + <string name="input_binding_description">按下按键或轻推摇杆,将其绑定到 %1$s 。</string> + <string name="input_binding_description_vertical_axis">向上或向下轻推您的摇杆。</string> + <string name="input_binding_description_horizontal_axis">向左或向右轻推您的摇杆。</string> + <string name="input_message_analog_only">此操作只能绑定到游戏手柄的摇杆或十字方向键上!</string> + <string name="input_message_button_only">此操作只能绑定到游戏手柄的按键上!</string> + + <!-- Generic buttons (Shared with lots of stuff) --> + <string name="generic_buttons">按键</string> + + <!-- Premium settings strings --> + <string name="design">更改主题(明亮,黑暗)</string> + <string name="design_updated">此项更改将在退出设置后生效。</string> + + <!-- Core settings strings --> + <string name="cpu_jit">启用 CPU JIT</string> + <string name="cpu_jit_description">使用即时编译进行 CPU 仿真。启用后,游戏性能将显著提高。</string> + <string name="init_clock">系统时钟类型</string> + <string name="init_clock_description">设置为“设备时钟”将使用设备的实际时钟,而设置为“模拟时钟”将在自定义的日期和时间进行游戏。</string> + + <!-- System settings strings --> + <string name="init_time">自定义系统时间</string> + <string name="init_time_description">如果将“系统时钟类型”设置为“模拟时钟”,可以修改时钟的日期和时间。</string> + <string name="emulated_region">模拟区域</string> + <string name="emulated_language">模拟语言</string> + + <!-- Camera settings strings --> + <string name="inner_camera">内置摄像头</string> + <string name="outer_left_camera">外置左摄像头</string> + <string name="outer_right_camera">外置右摄像头</string> + <string name="image_source">图像来源</string> + <string name="image_source_description">设置虚拟摄像头的图像来源。你可以选择一张图片或一个真实的摄像头。</string> + <string name="camera_device">实体摄像设备</string> + <string name="camera_device_description">如果将“图像来源”设置为“实体摄像设备”,将会使用设备的摄像头作为图像来源。</string> + <string name="camera_facing_front">前置摄像头</string> + <string name="camera_facing_back">后置摄像头</string> + <string name="camera_facing_external">外置摄像头</string> + <string name="image_flip">翻转图像</string> + + <!-- Graphics settings strings --> + <string name="vsync">启用垂直同步</string> + <string name="vsync_description">将游戏帧率与设备的屏幕刷新率同步。</string> + <string name="linear_filtering">启用线性过滤</string> + <string name="linear_filtering_description">开启后,游戏视觉效果会更加平滑。</string> + <string name="texture_filter_name">纹理滤镜</string> + <string name="texture_filter_description">通过对纹理使用滤镜来增强游戏的视觉效果。支持的滤镜有 Anime4K Ultrafast, Bicubic, ScaleForce 和 xBRZ freescale。</string> + <string name="hw_renderer">启用硬件渲染器</string> + <string name="hw_renderer_description">使用硬件模拟 3DS 图形。启用后,游戏性能将显著提高。</string> + <string name="hw_shaders">启用硬件着色器</string> + <string name="hw_shaders_description">使用硬件模拟 3DS 着色器。启用后,游戏性能将显著提高。</string> + <string name="shaders_accurate_mul">启用精确乘法运算</string> + <string name="shaders_accurate_mul_description">在硬件着色器中使用更加精确的乘法运算,这可能会修复一些图形错误。启用后,性能将有所降低。</string> + <string name="asynchronous_gpu">启用 GPU 异步仿真</string> + <string name="asynchronous_gpu_description">使用一个单独线程异步模拟 GPU 。启用后,游戏性能将有所提高。</string> + <string name="frame_limit_enable">启用运行速度限制</string> + <string name="frame_limit_enable_description">启用时,运行速度将被限制为正常速度的指定百分比。</string> + <string name="frame_limit_slider">限制运行速度百分比</string> + <string name="frame_limit_slider_description">指定限制运行速度的百分比。默认情况下,100% 将限制为正常运行速度。较高或较低的值将增加或降低运行速度。</string> + <string name="internal_resolution">内部分辨率</string> + <string name="internal_resolution_description">指定用于渲染的分辨率。高分辨率将大大提高图像质量,但需要相当强大的性能,某些游戏还可能因此导致故障。</string> + <string name="performance_warning">关闭此项将显著降低模拟性能!为了获得最佳体验,建议启用。</string> + <string name="debug_warning">警告:更改这些设置将降低模拟速度!</string> + + <!-- Premium strings --> + <string name="premium_text">高级版</string> + <string name="premium_settings_upsell">升级到高级版本以支持 Citra!</string> + <string name="premium_settings_upsell_description">升级到高级版本后,您将支持开发者继续改进 Citra,并获得这些高级功能!</string> + <string name="premium_settings_welcome">欢迎使用高级版本!</string> + <string name="premium_settings_welcome_description">感谢您对我们的支持!</string> + + <!-- Audio settings strings --> + <string name="audio_stretch">启用音频拉伸</string> + <string name="audio_stretch_description">拉伸音频以减少卡顿。启用后,会增加音频延迟并略微降低性能。</string> + + <!-- Miscellaneous --> + <string name="clear">清除</string> + <string name="slider_default">默认</string> + <string name="ini_saved">已保存的设置</string> + <string name="gameid_saved">已将设置保存于 %1$s 中</string> + <string name="error_saving">保存 %1$s.ini 失败:%2$s</string> + + <!-- Game Grid Screen--> + <string name="grid_menu_core_settings">设置</string> + + <!-- Add Directory Screen--> + <string name="select_game_folder">选择游戏目录</string> + <string name="add_directory_title">添加文件夹到库中</string> + + <!-- Preferences Screen --> + <string name="preferences_settings">设置</string> + <string name="preferences_premium">高级</string> + <string name="preferences_general">通用</string> + <string name="preferences_system">系统</string> + <string name="preferences_camera">摄像头</string> + <string name="preferences_controls">游戏手柄</string> + <string name="preferences_graphics">图形</string> + <string name="preferences_audio">声音</string> + <string name="preferences_debug">调试</string> + + <!-- ROM loading errors --> + <string name="loader_error_encrypted">您的 ROM 是加密的。</string> + <string name="loader_error_invalid_format">无效的 ROM 格式</string> + + <!-- Emulation Menu --> + <string name="emulation_show_fps">显示 FPS</string> + <string name="emulation_configure_controls">控制设置</string> + <string name="emulation_edit_layout">编辑布局</string> + <string name="emulation_done">完成</string> + <string name="emulation_toggle_controls">切换控制</string> + <string name="emulation_control_scale">调整大小</string> + <string name="emulation_open_settings">打开设置项</string> + <string name="emulation_switch_screen_layout">屏幕布局</string> + <string name="emulation_screen_layout_landscape">默认</string> + <string name="emulation_screen_layout_portrait">纵向</string> + <string name="emulation_screen_layout_single">单个屏幕</string> + <string name="emulation_screen_layout_sidebyside">并排屏幕</string> + <string name="emulation_swap_screens">交换上下屏</string> + <string name="emulation_touch_overlay_reset">重置虚拟按键</string> + <string name="emulation_show_overlay">显示虚拟按键</string> + <string name="emulation_close_game">关闭游戏</string> + <string name="emulation_close_game_message">您确定要关闭当前的游戏吗?</string> + <string name="menu_emulation_amiibo">Amiibo</string> + <string name="menu_emulation_amiibo_load">加载</string> + <string name="menu_emulation_amiibo_remove">移除</string> + <string name="select_amiibo">选择 Amiibo 文件</string> + <string name="amiibo_load_error">加载 Amiibo 时出错</string> + <string name="amiibo_load_error_message">加载该 Amiibo 文件时,发生了一个错误。请检查文件是否完整无错误。</string> + + <string name="write_permission_needed">您需要允许 Citra 对外部存储器进行读写访问,以便模拟器正常工作。</string> + <string name="load_settings">正在加载设置...</string> + + <string name="external_storage_not_mounted">需要有可用的外部存储器,Citra 才能正常工作。</string> + + <string name="select_dir">选择此目录</string> + <string name="empty_gamelist">找不到游戏文件或尚未添加游戏目录。</string> + + <!-- Software Keyboard --> + <string name="software_keyboard">软件键盘</string> + <string name="i_forgot">我忘记了</string> + <string name="fixed_length_required">文本长度不正确(应为 %d 个字符)</string> + <string name="max_length_exceeded">输入文本过长(不应超过 %d 个字符)</string> + <string name="blank_input_not_allowed">不允许空白输入</string> + <string name="empty_input_not_allowed">请输入字符</string> + + <!-- Mii Selector --> + <string name="mii_selector">Mii 选择器</string> + <string name="standard_mii">标准 Mii</string> + + <!-- Camera --> + <string name="camera_select_image">选择图像</string> + <string name="camera">摄像头</string> + <string name="camera_permission_needed">Citra 需要访问您的摄像头来模拟 3DS 上的摄像头。\n\n或者,您也可以在设置—>摄像头中将“图像来源”设置为“静止图像”。</string> +</resources> diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml new file mode 100644 index 000000000..c948e6a8b --- /dev/null +++ b/src/android/app/src/main/res/values/arrays.xml @@ -0,0 +1,174 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- All lists for ListPreference keys/values are placed here --> +<resources> + <string-array name="systemClockNames" translatable="true"> + <item>Device clock</item> + <item>Simulated clock</item> + </string-array> + + <integer-array name="systemClockValues" translatable="false"> + <item>0</item> + <item>1</item> + </integer-array> + + <string-array name="designNames" translatable="true"> + <item>Light</item> + <item>Dark</item> + <item>System default</item> + </string-array> + + <integer-array name="designValues" translatable="false"> + <item>0</item> + <item>1</item> + <item>2</item> + </integer-array> + + <!-- Pre-Android 10 does not support System Default --> + <string-array name="designNamesOld" translatable="true"> + <item>Light</item> + <item>Dark</item> + </string-array> + + <!-- Pre-Android 10 does not support System Default --> + <integer-array name="designValuesOld" translatable="false"> + <item>0</item> + <item>1</item> + </integer-array> + + <string-array name="regionNames"> + <item>Auto-select</item> + <item>Japan</item> + <item>USA</item> + <item>Europe</item> + <item>Australia</item> + <item>China</item> + <item>Korea</item> + <item>Taiwan</item> + </string-array> + + <integer-array name="regionValues"> + <item>-1</item> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + <item>4</item> + <item>5</item> + <item>6</item> + </integer-array> + + <string-array name="languageNames"> + <item>Japanese (日本語)</item> + <item>English</item> + <item>French (français)</item> + <item>German (Deutsch)</item> + <item>Italian (italiano)</item> + <item>Spanish (español)</item> + <item>Simplified Chinese (简体中文)</item> + <item>Korean (한국어)</item> + <item>Dutch (Nederlands)</item> + <item>Portuguese (português)</item> + <item>Russian (Русский)</item> + <item>Traditional Chinese (正體中文)</item> + </string-array> + + <integer-array name="languageValues"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + <item>4</item> + <item>5</item> + <item>6</item> + <item>7</item> + <item>8</item> + <item>9</item> + <item>10</item> + <item>11</item> + </integer-array> + + <string-array name="n3dsButtons"> + <item>a</item> + <item>b</item> + <item>x</item> + <item>y</item> + <item>L</item> + <item>R</item> + <item>ZL</item> + <item>ZR</item> + <item>Start</item> + <item>Select</item> + <item>D-Pad</item> + <item>Circle Pad</item> + <item>C Stick</item> + </string-array> + + <string-array name="cameraImageSourceNames"> + <item>Blank</item> + <item>Still Image</item> + <item>Device Camera</item> + </string-array> + + <string-array name="cameraImageSourceValues"> + <item>blank</item> + <item>image</item> + <item>ndk</item> + </string-array> + + <string-array name="cameraDeviceNames"> + <item>Default</item> + <item>Any Front Camera</item> + <item>Any Back Camera</item> + </string-array> + + <string-array name="cameraDeviceValues"> + <item /> + <item>_front</item> + <item>_back</item> + </string-array> + + <string-array name="cameraFlipNames"> + <item>None</item> + <item>Horizontal</item> + <item>Vertical</item> + <item>Reverse</item> + </string-array> + + <integer-array name="cameraFlipValues"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + </integer-array> + + <string-array name="audioInputTypeNames"> + <item>None</item> + <item>Real Device</item> + <item>Static Noise</item> + </string-array> + + <integer-array name="audioInputTypeValues"> + <item>0</item> + <item>1</item> + <item>2</item> + </integer-array> + + <string-array name="render3dModes"> + <item>Off</item> + <item>Side by Side</item> + <item>Anaglyph</item> + <item>Interlaced</item> + <item>Reverse Interlaced</item> + <item>Cardboard VR</item> + </string-array> + + <integer-array name="render3dValues"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + <item>4</item> + <item>5</item> + </integer-array> +</resources> diff --git a/src/android/app/src/main/res/values/colors.xml b/src/android/app/src/main/res/values/colors.xml index d0d2e5b1a..6668288a7 100644 --- a/src/android/app/src/main/res/values/colors.xml +++ b/src/android/app/src/main/res/values/colors.xml @@ -1,15 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <color name="citra_orange">#fec303</color> - <color name="citra_orange_dark">#fe8a03</color> + <color name="citra_orange">#FFC303</color> + <color name="citra_orange_dark">#FF8D03</color> + <color name="citra_accent">#CC7102</color> - <color name="dolphin_accent_wii">#9e9e9e</color> - <color name="dolphin_accent_wiiware">#2979ff</color> - <color name="dolphin_accent_gamecube">#651fff</color> + <color name="card_view_background">#ffffff</color> + <color name="card_view_disabled">#D5D5D5</color> - <color name="circle_grey">#bdbdbd</color> + <color name="gamelist_divider">#ffffff</color> - <color name="tv_card_unselected">#444444</color> + <color name="header_text">#1C1424</color> + <color name="header_subtext">#5C5661</color> + <color name="citra_logo_text_color">@color/header_text</color> </resources> diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..b3d186d88 --- /dev/null +++ b/src/android/app/src/main/res/values/dimens.xml @@ -0,0 +1,10 @@ +<resources> + <!-- Default screen margins, per the Android Design guidelines. --> + <dimen name="activity_horizontal_margin">16dp</dimen> + + <dimen name="spacing_small">4dp</dimen> + <dimen name="spacing_medlarge">12dp</dimen> + <dimen name="spacing_large">16dp</dimen> + + <dimen name="dialog_margin">20dp</dimen> +</resources> diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml new file mode 100644 index 000000000..9f6d8492e --- /dev/null +++ b/src/android/app/src/main/res/values/integers.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <integer name="game_grid_columns">1</integer> + + <!-- Default N3DS landscape layout --> + <integer name="N3DS_BUTTON_A_X">930</integer> + <integer name="N3DS_BUTTON_A_Y">620</integer> + <integer name="N3DS_BUTTON_B_X">870</integer> + <integer name="N3DS_BUTTON_B_Y">720</integer> + <integer name="N3DS_BUTTON_X_X">870</integer> + <integer name="N3DS_BUTTON_X_Y">520</integer> + <integer name="N3DS_BUTTON_Y_X">810</integer> + <integer name="N3DS_BUTTON_Y_Y">620</integer> + <integer name="N3DS_BUTTON_UP_X">15</integer> + <integer name="N3DS_BUTTON_UP_Y">470</integer> + <integer name="N3DS_TRIGGER_L_X">13</integer> + <integer name="N3DS_TRIGGER_L_Y">0</integer> + <integer name="N3DS_BUTTON_ZL_X">13</integer> + <integer name="N3DS_BUTTON_ZL_Y">110</integer> + <integer name="N3DS_TRIGGER_R_X">895</integer> + <integer name="N3DS_TRIGGER_R_Y">0</integer> + <integer name="N3DS_BUTTON_ZR_X">895</integer> + <integer name="N3DS_BUTTON_ZR_Y">110</integer> + <integer name="N3DS_STICK_C_X">740</integer> + <integer name="N3DS_STICK_C_Y">770</integer> + <integer name="N3DS_STICK_MAIN_X">100</integer> + <integer name="N3DS_STICK_MAIN_Y">670</integer> + <integer name="N3DS_BUTTON_SELECT_X">470</integer> + <integer name="N3DS_BUTTON_SELECT_Y">850</integer> + <integer name="N3DS_BUTTON_START_X">550</integer> + <integer name="N3DS_BUTTON_START_Y">850</integer> + <integer name="N3DS_BUTTON_HOME_X">450</integer> + <integer name="N3DS_BUTTON_HOME_Y">850</integer> + + <!-- Default N3DS portrait layout --> + <integer name="N3DS_BUTTON_A_PORTRAIT_X">810</integer> + <integer name="N3DS_BUTTON_A_PORTRAIT_Y">870</integer> + <integer name="N3DS_BUTTON_B_PORTRAIT_X">710</integer> + <integer name="N3DS_BUTTON_B_PORTRAIT_Y">925</integer> + <integer name="N3DS_BUTTON_X_PORTRAIT_X">710</integer> + <integer name="N3DS_BUTTON_X_PORTRAIT_Y">815</integer> + <integer name="N3DS_BUTTON_Y_PORTRAIT_X">610</integer> + <integer name="N3DS_BUTTON_Y_PORTRAIT_Y">870</integer> + <integer name="N3DS_BUTTON_UP_PORTRAIT_X">10</integer> + <integer name="N3DS_BUTTON_UP_PORTRAIT_Y">680</integer> + <integer name="N3DS_TRIGGER_L_PORTRAIT_X">10</integer> + <integer name="N3DS_TRIGGER_L_PORTRAIT_Y">0</integer> + <integer name="N3DS_BUTTON_ZL_PORTRAIT_X">10</integer> + <integer name="N3DS_BUTTON_ZL_PORTRAIT_Y">70</integer> + <integer name="N3DS_TRIGGER_R_PORTRAIT_X">810</integer> + <integer name="N3DS_TRIGGER_R_PORTRAIT_Y">0</integer> + <integer name="N3DS_BUTTON_ZR_PORTRAIT_X">810</integer> + <integer name="N3DS_BUTTON_ZR_PORTRAIT_Y">70</integer> + <integer name="N3DS_STICK_C_PORTRAIT_X">800</integer> + <integer name="N3DS_STICK_C_PORTRAIT_Y">710</integer> + <integer name="N3DS_STICK_MAIN_PORTRAIT_X">80</integer> + <integer name="N3DS_STICK_MAIN_PORTRAIT_Y">840</integer> + <integer name="N3DS_BUTTON_HOME_PORTRAIT_X">360</integer> + <integer name="N3DS_BUTTON_HOME_PORTRAIT_Y">794</integer> + <integer name="N3DS_BUTTON_SELECT_PORTRAIT_X">400</integer> + <integer name="N3DS_BUTTON_SELECT_PORTRAIT_Y">794</integer> + <integer name="N3DS_BUTTON_START_PORTRAIT_X">520</integer> + <integer name="N3DS_BUTTON_START_PORTRAIT_Y">794</integer> + +</resources> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..5d8f83182 --- /dev/null +++ b/src/android/app/src/main/res/values/strings.xml @@ -0,0 +1,226 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- General application strings --> + <string name="app_name" translatable="false">Citra</string> + <string name="app_disclaimer">This software will run games for the Nintendo 3DS handheld game console. No game titles are included.\n\nBefore you run, please place your rightfully owned 3DS game files onto your device storage.</string> + <string name="app_notification_channel_name" translatable="false">Citra</string> + <string name="app_notification_channel_id" translatable="false">Citra</string> + <string name="app_notification_channel_description">Citra 3DS emulator notifications</string> + <string name="app_notification_running">Citra is running</string> + + <!-- Input related strings --> + <string name="controller_circlepad">Circle Pad</string> + <string name="controller_c">C-Stick</string> + <string name="controller_triggers">Triggers</string> + <string name="controller_dpad">D-Pad</string> + <string name="controller_axis_vertical">Up/Down Axis</string> + <string name="controller_axis_horizontal">Left/Right Axis</string> + <string name="input_binding">Input Binding</string> + <string name="input_binding_description">Press or move an input to bind it to %1$s.</string> + <string name="input_binding_description_vertical_axis">Move your joystick up or down.</string> + <string name="input_binding_description_horizontal_axis">Move your joystick left or right.</string> + <string name="button_a" translatable="false">A</string> + <string name="button_b" translatable="false">B</string> + <string name="button_select" translatable="false">SELECT</string> + <string name="button_start" translatable="false">START</string> + <string name="button_x" translatable="false">X</string> + <string name="button_y" translatable="false">Y</string> + <string name="button_l" translatable="false">L</string> + <string name="button_r" translatable="false">R</string> + <string name="button_zl" translatable="false">ZL</string> + <string name="button_zr" translatable="false">ZR</string> + <string name="input_message_analog_only">This control must be bound to a gamepad analog stick or D-pad axis!</string> + <string name="input_message_button_only">This control must be bound to a gamepad button!</string> + + <!-- Generic buttons (Shared with lots of stuff) --> + <string name="generic_buttons">Buttons</string> + + <!-- Premium settings strings --> + <string name="design">Change Theme (Light, Dark)</string> + <string name="design_updated">Theme will update when exiting Settings</string> + + <!-- Core settings strings --> + <string name="cpu_jit">Enable CPU JIT</string> + <string name="cpu_jit_description">Uses the Just-in-Time (JIT) compiler for CPU emulation. When enabled, game performance will be significantly improved.</string> + <string name="init_clock">System clock type</string> + <string name="init_clock_description">Set the emulated 3DS clock to either reflect that of your device or start at a simulated date and time.</string> + + <!-- System settings strings --> + <string name="init_time">System clock starting time override</string> + <string name="init_time_description">If the \"System clock type\" setting is set to \"Simulated clock\", this changes the fixed date and time to start at.</string> + <string name="emulated_region">Emulated region</string> + <string name="emulated_language">Emulated language</string> + + <!-- Camera settings strings --> + <string name="inner_camera">Inner Camera</string> + <string name="outer_left_camera">Outer Left Camera</string> + <string name="outer_right_camera">Outer Right Camera</string> + <string name="image_source">Image Source</string> + <string name="image_source_description">Sets the image source of the virtual camera. You can use an image file, or a device camera when supported.</string> + <string name="camera_device">Camera Device</string> + <string name="camera_device_description">If the \"Image Source\" setting is set to \"Device Camera\", this sets the physical camera to use.</string> + <string name="camera_facing_front">Front</string> + <string name="camera_facing_back">Back</string> + <string name="camera_facing_external">External</string> + <string name="image_flip">Image Flip</string> + + <!-- Graphics settings strings --> + <string name="renderer">Renderer</string> + <string name="vsync">Enable V-Sync</string> + <string name="vsync_description">Synchronizes the game frame rate to the refresh rate of your device.</string> + <string name="linear_filtering">Enable linear filtering</string> + <string name="linear_filtering_description">Enables linear filtering, which causes game visuals to appear smoother.</string> + <string name="texture_filter_name">Texture Filter</string> + <string name="texture_filter_description">Enhances the visuals of games by applying a filter to textures. The supported filters are Anime4K Ultrafast, Bicubic, ScaleForce, and xBRZ freescale.</string> + <string name="hw_renderer">Enable hardware renderer</string> + <string name="hw_renderer_description">Uses hardware to emulate 3DS graphics. When enabled, game performance will be significantly improved.</string> + <string name="hw_shaders">Enable hardware shader</string> + <string name="hw_shaders_description">Uses hardware to emulate 3DS shaders. When enabled, game performance will be significantly improved.</string> + <string name="shaders_accurate_mul">Enable accurate shader multiplication</string> + <string name="shaders_accurate_mul_description">Uses more accurate multiplication in hardware shaders, which may fix some graphical bugs. When enabled, performance will be reduced.</string> + <string name="asynchronous_gpu">Enable asynchronous GPU emulation</string> + <string name="asynchronous_gpu_description">Uses a separate thread to emulate the GPU asynchronously. When enabled, performance will be improved.</string> + <string name="frame_limit_enable">Enable limit speed</string> + <string name="frame_limit_enable_description">When enabled, emulation speed will be limited to a specified percentage of normal speed.</string> + <string name="frame_limit_slider">Limit speed percent</string> + <string name="frame_limit_slider_description">Specifies the percentage to limit emulation speed. With the default of 100% emulation will be limited to normal speed. Values higher or lower will increase or decrease the speed limit.</string> + <string name="internal_resolution">Internal resolution</string> + <string name="internal_resolution_description">Specifies the resolution used to render at. A high resolution will improve visual quality a lot but is also quite heavy on performance and might cause glitches in certain games.</string> + <string name="performance_warning">Turning off this setting will significantly reduce emulation performance! For the best experience, it is recommended that you leave this setting enabled.</string> + <string name="debug_warning">Warning: Modifying these settings will slow emulation</string> + <string name="stereoscopy">Stereoscopy</string> + <string name="render3d">Stereoscopic 3D Mode</string> + <string name="factor3d">Depth</string> + <string name="factor3d_description">Specifies the value of the 3D slider. This should be set to higher than 0% when Stereoscopic 3D is enabled.</string> + <string name="cardboard_vr">Cardboard VR</string> + <string name="cardboard_screen_size">Cardboard Screen size</string> + <string name="cardboard_screen_size_description">Scales the screen to a percentage of its original size.</string> + <string name="cardboard_x_shift">Horizontal shift</string> + <string name="cardboard_x_shift_description">Specifies the percentage of empty space to shift the screens horizontally. Positive values move the two eyes closer to the middle, while negative values move them away.</string> + <string name="cardboard_y_shift">Vertical shift</string> + <string name="cardboard_y_shift_description">Specifies the percentage of empty space to shift the screens vertically. Positive values move the two eyes towards the bottom, while negative values move them towards the top.</string> + <string name="use_shader_jit">Use shader JIT</string> + <string name="use_disk_shader_cache">Use disk shader cache</string> + <string name="use_disk_shader_cache_description">Reduce stuttering by storing and loading generated shaders to disk. It cannot be used without Enabling Hardware Shader.</string> + + <!-- Premium strings --> + <string name="premium_text">Premium</string> + <string name="premium_settings_upsell">Upgrade to Premium and support Citra!</string> + <string name="premium_settings_upsell_description">With Premium, you will support the developers to continue improving Citra, and gain access to these exclusive features!</string> + <string name="premium_settings_welcome">Welcome to Premium.</string> + <string name="premium_settings_welcome_description">Thank you for your support!</string> + + <!-- Audio settings strings --> + <string name="audio_stretch">Enable audio stretching</string> + <string name="audio_stretch_description">Stretches audio to reduce stuttering. When enabled, increases audio latency and slightly reduces performance.</string> + <string name="audio_input_type">Audio Input Device</string> + + <!-- Miscellaneous --> + <string name="clear">Clear</string> + <string name="slider_default">Default</string> + <string name="ini_saved">Saved settings</string> + <string name="gameid_saved">Saved settings for %1$s</string> + <string name="error_saving">Error saving %1$s.ini: %2$s</string> + <string name="loading">Loading...</string> + + <!-- Game Grid Screen--> + <string name="grid_menu_core_settings">Settings</string> + + <!-- Add Directory Screen--> + <string name="select_game_folder">Select Game Folder</string> + <string name="install_cia_title">Install CIA</string> + + <!-- Preferences Screen --> + <string name="preferences_settings">Settings</string> + <string name="preferences_premium">Premium</string> + <string name="preferences_general">General</string> + <string name="preferences_system">System</string> + <string name="preferences_camera">Camera</string> + <string name="preferences_controls">Gamepad</string> + <string name="preferences_graphics">Graphics</string> + <string name="preferences_audio">Audio</string> + <string name="preferences_debug">Debug</string> + + <!-- ROM loading errors --> + <string name="loader_error_encrypted">Your ROM is encrypted</string> + <string name="loader_error_invalid_format">Invalid ROM format</string> + + <!-- Emulation Menu --> + <string name="emulation_save_state">Save State</string> + <string name="emulation_load_state">Load State</string> + <string name="emulation_empty_state_slot">Slot %1$d</string> + <string name="emulation_occupied_state_slot">Slot %1$d - %2$tF %2$tR</string> + <string name="emulation_show_fps">Show FPS</string> + <string name="emulation_configure_controls">Configure Controls</string> + <string name="emulation_edit_layout">Edit Layout</string> + <string name="emulation_done">Done</string> + <string name="emulation_toggle_controls">Toggle Controls</string> + <string name="emulation_control_scale">Adjust Scale</string> + <string name="emulation_control_joystick_rel_center">Relative Stick Center</string> + <string name="emulation_control_dpad_slide_enable">Enable D-Pad Sliding</string> + <string name="emulation_open_settings">Open Settings</string> + <string name="emulation_switch_screen_layout">Landscape Screen Layout</string> + <string name="emulation_screen_layout_landscape">Default</string> + <string name="emulation_screen_layout_portrait">Portrait</string> + <string name="emulation_screen_layout_single">Single Screen</string> + <string name="emulation_screen_layout_sidebyside">Side by Side Screens</string> + <string name="emulation_swap_screens">Swap Screens</string> + <string name="emulation_touch_overlay_reset">Reset Overlay</string> + <string name="emulation_show_overlay">Show Overlay</string> + <string name="emulation_close_game">Close Game</string> + <string name="emulation_close_game_message">Are you sure that you would like to close the current game?</string> + <string name="menu_emulation_amiibo">Amiibo</string> + <string name="menu_emulation_amiibo_load">Load</string> + <string name="menu_emulation_amiibo_remove">Remove</string> + <string name="select_amiibo">Select Amiibo file</string> + <string name="amiibo_load_error">Error loading Amiibo</string> + <string name="amiibo_load_error_message">While loading the specified Amiibo file, an error occurred. Please check that the file is correct.</string> + + <string name="write_permission_needed">You need to allow write access to external storage for the emulator to work</string> + <string name="load_settings">Loading Settings...</string> + + <string name="external_storage_not_mounted">The external storage needs to be available in order to use Citra</string> + + <string name="select_dir">Select This Directory</string> + <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string> + + <string name="do_not_show_this_again">Do not show this again</string> + <string name="savestate_warning_title">Savestates</string> + <string name="savestate_warning_message">Warning: Savestates are NOT a replacement for in-game saves, and are not meant to be reliable.\n\nUse at your own risk!</string> + + <!-- Software Keyboard --> + <string name="software_keyboard">Software Keyboard</string> + <string name="i_forgot">I Forgot</string> + <string name="fixed_length_required">Text length is not correct (should be %d characters)</string> + <string name="max_length_exceeded">Text is too long (should be no more than %d characters)</string> + <string name="blank_input_not_allowed">Blank input is not allowed</string> + <string name="empty_input_not_allowed">Empty input is not allowed</string> + + <!-- Mii Selector --> + <string name="mii_selector">Mii Selector</string> + <string name="standard_mii">Standard Mii</string> + + <!-- Camera --> + <string name="camera_select_image">Select Image</string> + <string name="camera">Camera</string> + <string name="camera_permission_needed">Citra needs to access your camera to emulate the 3DS\'s cameras.\n\nAlternatively, you can also set \"Image Source\" to \"Still Image\" in Camera Settings.</string> + + <!-- Microphone --> + <string name="microphone">Microphone</string> + <string name="microphone_permission_needed">Citra needs to access your microphone to emulate the 3DS\'s microphone.\n\nAlternatively, you can also change \"Audio Input Device\" in Audio Settings.</string> + + <!-- Core Errors --> + <string name="abort_button">Abort</string> + <string name="continue_button">Continue</string> + <string name="system_archive_not_found">System Archive Not Found</string> + <string name="system_archive_not_found_message">%s is missing. Please dump your system archives.\nContinuing emulation may result in crashes and bugs.</string> + <string name="system_archive_general">A system archive</string> + <string name="save_load_error">Save/Load Error</string> + <string name="fatal_error">Fatal Error</string> + <string name="fatal_error_message">A fatal error occurred. Check the log for details.\nContinuing emulation may result in crashes and bugs.</string> + + <!-- Disk shader cache --> + <string name="preparing_shaders">Preparing shaders</string> + <string name="building_shaders">Building shaders</string> +</resources> diff --git a/src/android/app/src/main/res/values/styles.xml b/src/android/app/src/main/res/values/styles.xml index 2243a9a69..47fe6f6ea 100644 --- a/src/android/app/src/main/res/values/styles.xml +++ b/src/android/app/src/main/res/values/styles.xml @@ -2,12 +2,15 @@ <resources> <!-- Inherit from the material theme --> - <style name="CitraBase" parent="Theme.AppCompat.Light.NoActionBar"> + <style name="CitraBase" parent="Theme.AppCompat.DayNight.NoActionBar"> <!-- Main theme colors --> <!-- Branding color for the app bar --> <item name="colorPrimary">@color/citra_orange</item> <!-- Darker variant for the status bar and contextual app bars --> <item name="colorPrimaryDark">@color/citra_orange_dark</item> + <item name="colorAccent">@color/citra_accent</item> + + <item name="titleTextColor">@color/citra_logo_text_color</item> <!-- Enable window content transitions --> <item name="android:windowContentTransitions">true</item> @@ -18,18 +21,17 @@ </style> <!-- Same as above, but use default action bar, and mandate margins. --> - <style name="CitraSettingsBase" parent="Theme.AppCompat.Light.DarkActionBar"> + <style name="CitraSettingsBase" parent="Theme.AppCompat.DayNight"> <item name="colorPrimary">@color/citra_orange</item> <item name="colorPrimaryDark">@color/citra_orange_dark</item> + <item name="colorAccent">@color/citra_accent</item> </style> - <!-- Themes for Dialogs --> - <!-- Inherit from the Base Citra Dialog Theme --> - - <style name="CitraEmulationBase" parent="Theme.AppCompat.Light.DarkActionBar"> + <style name="CitraEmulationBase" parent="Theme.AppCompat.DayNight"> <item name="colorPrimary">@color/citra_orange</item> <item name="colorPrimaryDark">@color/citra_orange_dark</item> + <item name="colorAccent">@color/citra_accent</item> <item name="android:windowTranslucentNavigation">true</item> <item name="android:windowBackground">@android:color/black</item> @@ -40,55 +42,24 @@ <item name="android:windowAllowReturnTransitionOverlap">true</item> </style> - <style name="CitraEmulationTvBase" parent="Theme.AppCompat.Light.NoActionBar"> + <!-- Inherit from a base file picker theme that handles day/night --> + <style name="FilePickerTheme" parent="FilePickerBaseTheme"> <item name="colorPrimary">@color/citra_orange</item> <item name="colorPrimaryDark">@color/citra_orange_dark</item> - <item name="android:windowTranslucentNavigation">true</item> + <item name="colorAccent">@color/citra_accent</item> + <item name="android:windowBackground">@color/card_view_background</item> - <item name="android:windowBackground">@android:color/black</item> + <!-- Need to set this also to style create folder dialog --> + <item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item> - <!-- Enable window content transitions --> - <item name="android:windowContentTransitions">true</item> - <item name="android:windowAllowEnterTransitionOverlap">true</item> - <item name="android:windowAllowReturnTransitionOverlap">true</item> + <item name="nnf_list_item_divider">@drawable/gamelist_divider</item> + <item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.DayNight.ActionBar</item> </style> - <!-- Hax to make Tablayout render icons --> - <style name="MyCustomTextAppearance" parent="TextAppearance.Design.Tab"> - <item name="textAllCaps">false</item> - </style> - - <!-- Android TV Themes --> - <style name="CitraTvBase" parent="Theme.Leanback.Browse"> + <style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.DayNight.Dialog.Alert"> <item name="colorPrimary">@color/citra_orange</item> <item name="colorPrimaryDark">@color/citra_orange_dark</item> - - <!-- Enable window content transitions --> - <item name="android:windowContentTransitions">true</item> - <item name="android:windowAllowEnterTransitionOverlap">true</item> - <item name="android:windowAllowReturnTransitionOverlap">true</item> - </style> - - <style name="InGameMenuOption" parent="Widget.AppCompat.Button.Borderless"> - <item name="android:textSize">16sp</item> - <item name="android:fontFamily">sans-serif-condensed</item> - <item name="android:textColor">@android:color/white</item> - <item name="android:textAllCaps">false</item> - <item name="android:layout_width">match_parent</item> - <item name="android:layout_height">48dp</item> - <item name="android:gravity">center_vertical|left</item> - <item name="android:paddingLeft">32dp</item> - <item name="android:paddingRight">32dp</item> - <item name="android:layout_margin">0dp</item> - </style> - - <style name="OverlayInGameMenuOption" parent="InGameMenuOption"> - <item name="android:textColor">@color/lb_control_button_text</item> - </style> - <style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert"> - <item name="colorPrimary">@color/citra_orange</item> - <item name="colorPrimaryDark">@color/citra_orange_dark</item> - <item name="colorAccent">@android:color/holo_purple</item> + <item name="colorAccent">@color/citra_accent</item> </style> </resources> diff --git a/src/android/app/src/main/res/values/styles_filepicker.xml b/src/android/app/src/main/res/values/styles_filepicker.xml new file mode 100644 index 000000000..0b0c3fe1a --- /dev/null +++ b/src/android/app/src/main/res/values/styles_filepicker.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="FilePickerBaseTheme" parent="NNF_BaseTheme.Light" /> +</resources> diff --git a/src/android/app/src/test/java/org/citra_emu/citra/ExampleUnitTest.java b/src/android/app/src/test/java/org/citra/citra_emu/ExampleUnitTest.java similarity index 91% rename from src/android/app/src/test/java/org/citra_emu/citra/ExampleUnitTest.java rename to src/android/app/src/test/java/org/citra/citra_emu/ExampleUnitTest.java index 066ffe6fd..4e4bb317f 100644 --- a/src/android/app/src/test/java/org/citra_emu/citra/ExampleUnitTest.java +++ b/src/android/app/src/test/java/org/citra/citra_emu/ExampleUnitTest.java @@ -1,4 +1,4 @@ -package org.citra_emu.citra; +package org.citra.citra_emu; import org.junit.Test; @@ -14,4 +14,4 @@ public class ExampleUnitTest { public void addition_isCorrect() { assertEquals(4, 2 + 2); } -} +} \ No newline at end of file diff --git a/src/android/build.gradle b/src/android/build.gradle index 3dc54b304..2ab96a995 100644 --- a/src/android/build.gradle +++ b/src/android/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.0' + classpath 'com.android.tools.build:gradle:4.1.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/src/android/code-style-java.xml b/src/android/code-style-java.xml index 74622e6af..a8ed003c8 100644 --- a/src/android/code-style-java.xml +++ b/src/android/code-style-java.xml @@ -1,9 +1,18 @@ <code_scheme name="Citra-Java" version="173"> + <option name="OTHER_INDENT_OPTIONS"> + <value> + <option name="INDENT_SIZE" value="2" /> + <option name="TAB_SIZE" value="2" /> + </value> + </option> <option name="RIGHT_MARGIN" value="100" /> <AndroidXmlCodeStyleSettings> <option name="USE_CUSTOM_SETTINGS" value="true" /> </AndroidXmlCodeStyleSettings> <JavaCodeStyleSettings> + <option name="FIELD_NAME_PREFIX" value="m_" /> + <option name="STATIC_FIELD_NAME_PREFIX" value="s_" /> + <option name="ANNOTATION_PARAMETER_WRAP" value="1" /> <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" /> <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" /> <option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND"> @@ -31,42 +40,45 @@ <emptyLine /> </value> </option> + <option name="JD_P_AT_EMPTY_LINES" value="false" /> </JavaCodeStyleSettings> - <Objective-C-extensions> - <file> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" /> - </file> - <class> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" /> - <option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" /> - </class> - <extensions> - <pair source="cpp" header="h" fileNamingConvention="NONE" /> - <pair source="c" header="h" fileNamingConvention="NONE" /> - </extensions> - </Objective-C-extensions> <XML> - <option name="XML_KEEP_LINE_BREAKS" value="false" /> - <option name="XML_ALIGN_ATTRIBUTES" value="false" /> - <option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" /> + <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" /> </XML> <codeStyleSettings language="JAVA"> - <option name="KEEP_LINE_BREAKS" value="false" /> - <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" /> - <option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" /> - <option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" /> + <option name="BRACE_STYLE" value="2" /> + <option name="CLASS_BRACE_STYLE" value="2" /> + <option name="METHOD_BRACE_STYLE" value="2" /> + <option name="LAMBDA_BRACE_STYLE" value="2" /> + <option name="ELSE_ON_NEW_LINE" value="true" /> + <option name="WHILE_ON_NEW_LINE" value="true" /> + <option name="CATCH_ON_NEW_LINE" value="true" /> + <option name="FINALLY_ON_NEW_LINE" value="true" /> + <option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> + <option name="CALL_PARAMETERS_WRAP" value="1" /> + <option name="METHOD_PARAMETERS_WRAP" value="1" /> + <option name="RESOURCE_LIST_WRAP" value="1" /> + <option name="EXTENDS_LIST_WRAP" value="1" /> + <option name="THROWS_LIST_WRAP" value="1" /> + <option name="EXTENDS_KEYWORD_WRAP" value="1" /> + <option name="THROWS_KEYWORD_WRAP" value="1" /> + <option name="METHOD_CALL_CHAIN_WRAP" value="1" /> + <option name="BINARY_OPERATION_WRAP" value="1" /> + <option name="TERNARY_OPERATION_WRAP" value="1" /> + <option name="FOR_STATEMENT_WRAP" value="1" /> + <option name="ASSIGNMENT_WRAP" value="1" /> + <option name="ASSERT_STATEMENT_WRAP" value="1" /> + <option name="DOWHILE_BRACE_FORCE" value="3" /> + <option name="METHOD_ANNOTATION_WRAP" value="1" /> + <option name="CLASS_ANNOTATION_WRAP" value="1" /> + <option name="FIELD_ANNOTATION_WRAP" value="1" /> + <option name="PARAMETER_ANNOTATION_WRAP" value="1" /> + <option name="VARIABLE_ANNOTATION_WRAP" value="1" /> + <option name="ENUM_CONSTANTS_WRAP" value="1" /> + <indentOptions> + <option name="INDENT_SIZE" value="2" /> + <option name="TAB_SIZE" value="2" /> + </indentOptions> </codeStyleSettings> <codeStyleSettings language="XML"> <option name="FORCE_REARRANGE_MODE" value="1" /> @@ -80,7 +92,7 @@ <match> <AND> <NAME>xmlns:android</NAME> - <XML_NAMESPACE>^$</XML_NAMESPACE> + <XML_NAMESPACE>Namespace:</XML_NAMESPACE> </AND> </match> </rule> @@ -90,7 +102,7 @@ <match> <AND> <NAME>xmlns:.*</NAME> - <XML_NAMESPACE>^$</XML_NAMESPACE> + <XML_NAMESPACE>Namespace:</XML_NAMESPACE> </AND> </match> <order>BY_NAME</order> @@ -147,6 +159,59 @@ <order>BY_NAME</order> </rule> </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_width</NAME> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_height</NAME> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:layout_.*</NAME> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:width</NAME> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:height</NAME> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> <section> <rule> <match> @@ -155,7 +220,7 @@ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> </AND> </match> - <order>ANDROID_ATTRIBUTE_ORDER</order> + <order>BY_NAME</order> </rule> </section> <section> diff --git a/src/android/gradle.properties b/src/android/gradle.properties index 743d692ce..8de505811 100644 --- a/src/android/gradle.properties +++ b/src/android/gradle.properties @@ -6,6 +6,8 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. +android.enableJetifier=true +android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit diff --git a/src/android/gradle/wrapper/gradle-wrapper.properties b/src/android/gradle/wrapper/gradle-wrapper.properties index 5e8c8d351..8a9d3df19 100644 --- a/src/android/gradle/wrapper/gradle-wrapper.properties +++ b/src/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Dec 29 13:07:24 CST 2020 +#Sun Feb 21 18:16:59 EST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip