diff --git a/src/LibRyujinx/Android/AndroidLogTarget.cs b/src/LibRyujinx/Android/AndroidLogTarget.cs index 109886f82..c32cc261a 100644 --- a/src/LibRyujinx/Android/AndroidLogTarget.cs +++ b/src/LibRyujinx/Android/AndroidLogTarget.cs @@ -1,4 +1,4 @@ -using Ryujinx.Common.Logging; +using Ryujinx.Common.Logging; using Ryujinx.Common.Logging.Formatters; using Ryujinx.Common.Logging.Targets; using System; @@ -12,7 +12,7 @@ namespace LibRyujinx string ILogTarget.Name { get => _name; } - public AndroidLogTarget( string name) + public AndroidLogTarget(string name) { _name = name; _formatter = new DefaultLogFormatter(); diff --git a/src/LibRyujinx/Android/AndroidUiHandler.cs b/src/LibRyujinx/Android/AndroidUiHandler.cs new file mode 100644 index 000000000..945feb0f4 --- /dev/null +++ b/src/LibRyujinx/Android/AndroidUiHandler.cs @@ -0,0 +1,136 @@ +using LibHac.Tools.Fs; +using Ryujinx.Common.Logging; +using Ryujinx.HLE; +using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; +using Ryujinx.HLE.Ui; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LibRyujinx.Android +{ + internal class AndroidUiHandler : IHostUiHandler, IDisposable + { + public IHostUiTheme HostUiTheme => throw new NotImplementedException(); + + public ManualResetEvent _waitEvent; + public ManualResetEvent _responseEvent; + private bool _isDisposed; + private bool _isOkPressed; + private long _input; + + public AndroidUiHandler() + { + _waitEvent = new ManualResetEvent(false); + _responseEvent = new ManualResetEvent(false); + } + + public IDynamicTextInputHandler CreateDynamicTextInputHandler() + { + throw new NotImplementedException(); + } + + public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText) + { + LibRyujinx.setUiHandlerTitle(LibRyujinx.storeString(title ?? "")); + LibRyujinx.setUiHandlerMessage(LibRyujinx.storeString(message ?? "")); + LibRyujinx.setUiHandlerType(1); + + _responseEvent.Reset(); + Set(); + _responseEvent.WaitOne(); + + return _isOkPressed; + } + + public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText) + { + LibRyujinx.setUiHandlerTitle(LibRyujinx.storeString("Software Keyboard")); + LibRyujinx.setUiHandlerMessage(LibRyujinx.storeString(args.HeaderText ?? "")); + LibRyujinx.setUiHandlerWatermark(LibRyujinx.storeString(args.GuideText ?? "")); + LibRyujinx.setUiHandlerSubtitle(LibRyujinx.storeString(args.SubtitleText ?? "")); + LibRyujinx.setUiHandlerInitialText(LibRyujinx.storeString(args.InitialText ?? "")); + LibRyujinx.setUiHandlerMinLength(args.StringLengthMin); + LibRyujinx.setUiHandlerMaxLength(args.StringLengthMax); + LibRyujinx.setUiHandlerType(2); + LibRyujinx.setUiHandlerKeyboardMode((int)args.KeyboardMode); + + _responseEvent.Reset(); + Set(); + _responseEvent.WaitOne(); + + userText = _input != -1 ? LibRyujinx.GetStoredString(_input) : ""; + + return _isOkPressed; + } + + public bool DisplayMessageDialog(string title, string message) + { + LibRyujinx.setUiHandlerTitle(LibRyujinx.storeString(title ?? "")); + LibRyujinx.setUiHandlerMessage(LibRyujinx.storeString(message ?? "")); + LibRyujinx.setUiHandlerType(1); + + _responseEvent.Reset(); + Set(); + _responseEvent.WaitOne(); + + return _isOkPressed; + } + + public bool DisplayMessageDialog(ControllerAppletUiArgs args) + { + string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}"; + + string message = $"Application requests **{playerCount}** player(s) with:\n\n" + + $"**TYPES:** {args.SupportedStyles}\n\n" + + $"**PLAYERS:** {string.Join(", ", args.SupportedPlayers)}\n\n" + + (args.IsDocked ? "Docked mode set. `Handheld` is also invalid.\n\n" : "") + + "_Please reconfigure Input now and then press OK._"; + + return DisplayMessageDialog("Controller Applet", message); + } + + public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value) + { + // throw new NotImplementedException(); + } + + internal void Wait() + { + if (_isDisposed) + return; + _waitEvent.Reset(); + _waitEvent.WaitOne(); + _waitEvent.Reset(); + } + + internal void Set() + { + if (_isDisposed) + return; + _waitEvent.Set(); + } + + internal void SetResponse(bool isOkPressed, long input) + { + if (_isDisposed) + return; + _isOkPressed = isOkPressed; + _input = input; + _responseEvent.Set(); + } + + public void Dispose() + { + _isDisposed = true; + _waitEvent.Set(); + _waitEvent.Set(); + _responseEvent.Dispose(); + _waitEvent.Dispose(); + } + } +} diff --git a/src/LibRyujinx/Android/JniExportedMethods.cs b/src/LibRyujinx/Android/JniExportedMethods.cs index e548b5996..7be533091 100644 --- a/src/LibRyujinx/Android/JniExportedMethods.cs +++ b/src/LibRyujinx/Android/JniExportedMethods.cs @@ -1,4 +1,4 @@ -using LibRyujinx.Jni; +using LibRyujinx.Jni; using LibRyujinx.Jni.Pointers; using LibRyujinx.Jni.Primitives; using LibRyujinx.Jni.References; @@ -42,11 +42,36 @@ namespace LibRyujinx private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch); [DllImport("libryujinxjni")] - private extern static long storeString(string ch); - [DllImport("libryujinxjni")] - private extern static IntPtr getString(long id); + internal extern static long storeString(string ch); - private static string GetStoredString(long id) + [DllImport("libryujinxjni")] + internal extern static IntPtr getString(long id); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerTitle(long title); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerMessage(long message); + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerWatermark(long watermark); + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerInitialText(long text); + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerSubtitle(long text); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerType(int type); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerKeyboardMode(int mode); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerMinLength(int lenght); + + [DllImport("libryujinxjni")] + internal extern static long setUiHandlerMaxLength(int lenght); + + internal static string GetStoredString(long id) { var pointer = getString(id); if (pointer != IntPtr.Zero) @@ -84,21 +109,21 @@ namespace LibRyujinx } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_initialize")] - public static JBoolean JniInitialize(JEnvRef jEnv, JObjectLocalRef jObj, JLong jpathId, JBoolean enableDebugLogs) + public static JBoolean JniInitialize(JEnvRef jEnv, JObjectLocalRef jObj, JLong jpathId) { Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); PlatformInfo.IsBionic = true; Logger.AddTarget( new AsyncLogTargetWrapper( - new AndroidLogTarget("Ryujinx"), + new AndroidLogTarget("RyujinxLog"), 1000, AsyncLogTargetOverflowAction.Block )); var path = GetStoredString(jpathId); - var init = Initialize(path, enableDebugLogs); + var init = Initialize(path); _surfaceEvent?.Set(); @@ -237,7 +262,7 @@ namespace LibRyujinx } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")] - public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci) + public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JInt type, JInt updateDescriptor) { Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); if (SwitchDevice?.EmulationContext == null) @@ -246,8 +271,62 @@ namespace LibRyujinx } var stream = OpenFile(descriptor); + var update = updateDescriptor == -1 ? null : OpenFile(updateDescriptor); - return LoadApplication(stream, isXci); + return LoadApplication(stream, (FileType)(int)type, update); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceVerifyFirmware")] + public static JLong JniVerifyFirmware(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci) + { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); + + var stream = OpenFile(descriptor); + + long stringHandle = -1; + + try + { + var version = VerifyFirmware(stream, isXci); + + if (version != null) + { + stringHandle = storeString(version.VersionString); + } + } + catch(Exception _) + { + + } + + return stringHandle; + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceInstallFirmware")] + public static void JniInstallFirmware(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci) + { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); + + var stream = OpenFile(descriptor); + + InstallFirmware(stream, isXci); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetInstalledFirmwareVersion")] + public static JLong JniGetInstalledFirmwareVersion(JEnvRef jEnv, JObjectLocalRef jObj) + { + Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); + + var version = SwitchDevice?.ContentManager.GetCurrentFirmwareVersion(); + + long stringHandle = -1; + + if (version != null) + { + stringHandle = storeString(version.VersionString); + } + + return stringHandle; } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")] @@ -413,6 +492,13 @@ namespace LibRyujinx RunLoop(); } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_loggingSetEnabled")] + public static void JniSetLoggingEnabledNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt logLevel, JBoolean enabled) + { + Logger.SetEnable((LogLevel)(int)logLevel, enabled); + } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfoFromPath")] public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef path) { @@ -422,12 +508,12 @@ namespace LibRyujinx } [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")] - public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JInt fileDescriptor, JBoolean isXci) + public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JInt fileDescriptor, JLong extension) { Logger.Trace?.Print(LogClass.Application, "Jni Function Call"); using var stream = OpenFile(fileDescriptor); - - var info = GetGameInfo(stream, isXci); + var ext = GetStoredString(extension); + var info = GetGameInfo(stream, ext.ToLower()); return GetInfo(jEnv, info); } @@ -546,6 +632,20 @@ namespace LibRyujinx SetButtonReleased((GamepadButtonInputId)(int)button, id); } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetAccelerometerData")] + public static void JniSetAccelerometerData(JEnvRef jEnv, JObjectLocalRef jObj, JFloat x, JFloat y, JFloat z, JInt id) + { + var accel = new Vector3(x, y, z); + SetAccelerometerData(accel, id); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetGyroData")] + public static void JniSetGyroData(JEnvRef jEnv, JObjectLocalRef jObj, JFloat x, JFloat y, JFloat z, JInt id) + { + var gryo = new Vector3(x, y, z); + SetGryoData(gryo, id); + } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetStickAxis")] public static void JniSetStickAxis(JEnvRef jEnv, JObjectLocalRef jObj, JInt stick, JFloat x, JFloat y, JInt id) { @@ -635,6 +735,30 @@ namespace LibRyujinx DeleteUser(userId); } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerSetup")] + public static void JniSetupUiHandler(JEnvRef jEnv, JObjectLocalRef jObj) + { + SetupUiHandler(); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerWait")] + public static void JniWaitUiHandler(JEnvRef jEnv, JObjectLocalRef jObj) + { + WaitUiHandler(); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerStopWait")] + public static void JniStopUiHandlerWait(JEnvRef jEnv, JObjectLocalRef jObj) + { + StopUiHandlerWait(); + } + + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_uiHandlerSetResponse")] + public static void JniSetUiHandlerResponse(JEnvRef jEnv, JObjectLocalRef jObj, JBoolean isOkPressed, JLong input) + { + SetUiHandlerResponse(isOkPressed, input); + } + [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userOpenUser")] public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JLong userIdPtr) { diff --git a/src/RyujinxAndroid/app/build.gradle b/src/RyujinxAndroid/app/build.gradle index 606440096..2740c03c7 100644 --- a/src/RyujinxAndroid/app/build.gradle +++ b/src/RyujinxAndroid/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "org.ryujinx.android" minSdk 30 targetSdk 33 - versionCode 10004 - versionName '1.0.4' + versionCode 10010 + versionName '1.0.10' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -49,6 +49,7 @@ android { buildFeatures { compose true prefab true + buildConfig true } composeOptions { kotlinCompilerExtensionVersion '1.3.2' diff --git a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml index 549ed4ed2..0e4577594 100644 --- a/src/RyujinxAndroid/app/src/main/AndroidManifest.xml +++ b/src/RyujinxAndroid/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ + + + + + + + + + diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h index 836cb6ccc..ec83389a6 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h +++ b/src/RyujinxAndroid/app/src/main/cpp/ryuijnx.h @@ -38,6 +38,36 @@ void* _ryujinxNative = NULL; +class UiHandler { +public: + void setTitle(long storedTitle); + void setMessage(long storedMessage); + void setWatermark(long wm); + void setType(int t); + void setMode(int t); + void setMinLength(int t); + void setMaxLength(int t); + void setInitialText(long text); + void setSubtitle(long text); + + long getTitle(); + long getMessage(); + long getWatermark(); + long getInitialText(); + long getSubtitle(); + int type = 0; + int keyboardMode = 0; + int min_length = -1; + int max_length = -1; + +private: + long title = -1; + long message = -1; + long watermark = -1; + long initialText = -1; + long subtitle = -1; +}; + // Ryujinx imported functions bool (*initialize)(char*) = NULL; @@ -46,7 +76,7 @@ long _currentRenderingThreadId = 0; JavaVM* _vm = nullptr; jobject _mainActivity = nullptr; jclass _mainActivityClass = nullptr; -std::string _currentString = ""; string_helper str_helper = string_helper(); +UiHandler ui_handler = UiHandler(); #endif //RYUJINXNATIVE_RYUIJNX_H diff --git a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp index 391d71850..da87b8051 100644 --- a/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp +++ b/src/RyujinxAndroid/app/src/main/cpp/ryujinx.cpp @@ -185,6 +185,8 @@ void setProgressInfo(char* info, float progressValue) { progress = progressValue; } +bool isInitialOrientationFlipped = true; + extern "C" void setCurrentTransform(long native_window, int transform){ if(native_window == 0 || native_window == -1) @@ -201,10 +203,10 @@ void setCurrentTransform(long native_window, int transform){ nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY; break; case 0x2: - nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_90; + nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_90; break; case 0x4: - nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_180; + nativeTransform = isInitialOrientationFlipped ? ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY : ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_180; break; case 0x8: nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_270; @@ -325,6 +327,37 @@ const char* getString(long id){ return cstr; } +extern "C" +{ +void setUiHandlerTitle(long title) { + ui_handler.setTitle(title); +} +void setUiHandlerMessage(long message) { + ui_handler.setMessage(message); +} +void setUiHandlerWatermark(long wm) { + ui_handler.setWatermark(wm); +} +void setUiHandlerType(int type) { + ui_handler.setType(type); +} +void setUiHandlerKeyboardMode(int mode) { + ui_handler.setMode(mode); +} +void setUiHandlerMinLength(int length) { + ui_handler.setMinLength(length); +} +void setUiHandlerMaxLength(int length) { + ui_handler.setMaxLength(length); +} +void setUiHandlerInitialText(long text) { + ui_handler.setInitialText(text); +} +void setUiHandlerSubtitle(long text) { + ui_handler.setSubtitle(text); +} +} + extern "C" JNIEXPORT jlong JNICALL Java_org_ryujinx_android_NativeHelpers_storeStringJava(JNIEnv *env, jobject thiz, jstring string) { @@ -335,5 +368,154 @@ Java_org_ryujinx_android_NativeHelpers_storeStringJava(JNIEnv *env, jobject thiz extern "C" JNIEXPORT jstring JNICALL Java_org_ryujinx_android_NativeHelpers_getStringJava(JNIEnv *env, jobject thiz, jlong id) { - return createStringFromStdString(env, str_helper.get_stored(id)); + return createStringFromStdString(env, id > -1 ? str_helper.get_stored(id) : ""); +} + +extern "C" +JNIEXPORT void JNICALL +Java_org_ryujinx_android_NativeHelpers_setIsInitialOrientationFlipped(JNIEnv *env, jobject thiz, + jboolean is_flipped) { + isInitialOrientationFlipped = is_flipped; +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestType(JNIEnv *env, jobject thiz) { + return ui_handler.type; +} +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestTitle(JNIEnv *env, jobject thiz) { + return ui_handler.getTitle(); +} +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestMessage(JNIEnv *env, jobject thiz) { + return ui_handler.getMessage(); +} + + +void UiHandler::setTitle(long storedTitle) { + if(title != -1){ + str_helper.get_stored(title); + title = -1; + } + + title = storedTitle; +} + +void UiHandler::setMessage(long storedMessage) { + if(message != -1){ + str_helper.get_stored(message); + message = -1; + } + + message = storedMessage; +} + +void UiHandler::setType(int t) { + this->type = t; +} + +long UiHandler::getTitle() { + auto v = title; + title = -1; + return v; +} + +long UiHandler::getMessage() { + auto v = message; + message = -1; + return v; +} + +void UiHandler::setWatermark(long wm) { + if(watermark != -1){ + str_helper.get_stored(watermark); + watermark = -1; + } + + watermark = wm; +} + +void UiHandler::setMinLength(int t) { + this->min_length = t; +} + +void UiHandler::setMaxLength(int t) { + this->max_length = t; +} + +long UiHandler::getWatermark() { + auto v = watermark; + watermark = -1; + return v; +} + +void UiHandler::setInitialText(long text) { + if(initialText != -1){ + str_helper.get_stored(watermark); + initialText = -1; + } + + initialText = text; +} + +void UiHandler::setSubtitle(long text) { + if(subtitle != -1){ + str_helper.get_stored(subtitle); + subtitle = -1; + } + + subtitle = text; +} + +long UiHandler::getInitialText() { + auto v = initialText; + initialText = -1; + return v; +} + +long UiHandler::getSubtitle() { + auto v = subtitle; + subtitle = -1; + return v; +} + +void UiHandler::setMode(int t) { + keyboardMode = t; +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerMinLength(JNIEnv *env, jobject thiz) { + return ui_handler.min_length; +} +extern "C" +JNIEXPORT jint JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerMaxLength(JNIEnv *env, jobject thiz) { + return ui_handler.max_length; +} + +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestWatermark(JNIEnv *env, jobject thiz) { + return ui_handler.getWatermark(); +} + +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestInitialText(JNIEnv *env, jobject thiz) { + return ui_handler.getInitialText(); +} +extern "C" +JNIEXPORT jlong JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerRequestSubtitle(JNIEnv *env, jobject thiz) { + return ui_handler.getSubtitle(); +} + +extern "C" +JNIEXPORT jint JNICALL +Java_org_ryujinx_android_NativeHelpers_getUiHandlerKeyboardMode(JNIEnv *env, jobject thiz) { + return ui_handler.keyboardMode; } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt index d333a3ecc..e70053762 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/BaseActivity.kt @@ -1,16 +1,9 @@ package org.ryujinx.android -import android.os.Bundle -import android.os.PersistableBundle import androidx.activity.ComponentActivity abstract class BaseActivity : ComponentActivity() { companion object{ val crashHandler = CrashHandler() } - - override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { - Thread.setDefaultUncaughtExceptionHandler(crashHandler) - super.onCreate(savedInstanceState, persistentState) - } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt index 2d6503020..f6c2eb656 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/CrashHandler.kt @@ -8,6 +8,6 @@ class CrashHandler : UncaughtExceptionHandler { override fun uncaughtException(t: Thread, e: Throwable) { crashLog += e.toString() + "\n" - File(MainActivity.AppPath + "${File.separator}crash.log").writeText(crashLog) + File(MainActivity.AppPath + "${File.separator}Logs${File.separator}crash.log").writeText(crashLog) } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt index 2e77ad21b..204a58e6b 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameController.kt @@ -11,8 +11,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.isVisible -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.lifecycleScope import com.swordfish.radialgamepad.library.RadialGamePad import com.swordfish.radialgamepad.library.config.ButtonConfig @@ -29,6 +27,7 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.viewmodels.QuickSettings typealias GamePad = RadialGamePad typealias GamePadConfig = RadialGamePadConfig @@ -65,6 +64,7 @@ class GameController(var activity: Activity) { } controller.controllerView = c viewModel.setGameController(controller) + controller.setVisible(QuickSettings(viewModel.activity).useVirtualController) c }) } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt index 48cfb4ff6..9eca1df52 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/GameHost.kt @@ -8,7 +8,6 @@ import android.view.SurfaceView import androidx.compose.runtime.MutableState import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.MainViewModel -import org.ryujinx.android.viewmodels.QuickSettings import kotlin.concurrent.thread @SuppressLint("ViewConstructor") @@ -76,6 +75,8 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su _isInit = false _isStarted = false + mainViewModel.activity.uiHandler.stop() + _updateThread?.join() _renderingThreadWatcher?.join() } @@ -88,21 +89,16 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su _nativeRyujinx.inputInitialize(width, height) - val settings = QuickSettings(mainViewModel.activity) - - if (!settings.useVirtualController) { - mainViewModel.controller?.setVisible(false) - } else { - mainViewModel.controller?.connect() - } - - mainViewModel.physicalControllerManager?.connect() + val id = mainViewModel.physicalControllerManager?.connect() + mainViewModel.motionSensorManager?.setControllerId(id ?: -1) _nativeRyujinx.graphicsRendererSetSize( surfaceHolder.surfaceFrame.width(), surfaceHolder.surfaceFrame.height() ) + NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3) + _guestThread = thread(start = true) { runGame() } @@ -167,6 +163,10 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su mainViewModel.performanceManager?.closeCurrentRenderingSession() } } + + thread { + mainViewModel.activity.uiHandler.listen() + } _nativeRyujinx.graphicsRendererRunLoop() game?.close() diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt index 7cda8a3ac..77dab8240 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Helpers.kt @@ -72,6 +72,7 @@ class Helpers { } return null } + fun copyToData( file: DocumentFile, path: String, storageHelper: SimpleStorageHelper, isCopying: MutableState, @@ -79,17 +80,18 @@ class Helpers { currentProgressName: MutableState, finish: () -> Unit ) { + var fPath = path + "/${file.name}"; var callback: FileCallback? = object : FileCallback() { override fun onFailed(errorCode: FileCallback.ErrorCode) { super.onFailed(errorCode) - File(path).delete() + File(fPath).delete() finish() } override fun onStart(file: Any, workerThread: Thread): Long { copyProgress.value = 0f - (file as DocumentFile)?.apply { + (file as DocumentFile).apply { currentProgressName.value = "Copying ${file.name}" } return super.onStart(file, workerThread) @@ -98,7 +100,7 @@ class Helpers { override fun onReport(report: Report) { super.onReport(report) - if(!isCopying.value) { + if (!isCopying.value) { Thread.currentThread().interrupt() } @@ -113,17 +115,16 @@ class Helpers { } val ioScope = CoroutineScope(Dispatchers.IO) isCopying.value = true + File(fPath).delete() file.apply { - if (!File(path + "/${file.name}").exists()) { - val f = this - ioScope.launch { - f.copyFileTo( - storageHelper.storage.context, - File(path), - callback = callback!! - ) + val f = this + ioScope.launch { + f.copyFileTo( + storageHelper.storage.context, + File(path), + callback = callback!! + ) - } } } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Logging.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Logging.kt new file mode 100644 index 000000000..bc8c615ab --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/Logging.kt @@ -0,0 +1,58 @@ +package org.ryujinx.android + +import android.content.Intent +import androidx.core.content.FileProvider +import net.lingala.zip4j.ZipFile +import org.ryujinx.android.viewmodels.MainViewModel +import java.io.File +import java.net.URLConnection + +class Logging(private var viewModel: MainViewModel) { + val logPath = MainActivity.AppPath + "/Logs" + init{ + File(logPath).mkdirs() + } + + fun requestExport(){ + val files = File(logPath).listFiles() + files?.apply { + val zipExportPath = MainActivity.AppPath + "/log.zip" + File(zipExportPath).delete() + var count = 0 + if (files.isNotEmpty()) { + val zipFile = ZipFile(zipExportPath) + for (file in files) { + if(file.isFile) { + zipFile.addFile(file) + count++ + } + } + zipFile.close() + } + if (count > 0) { + val zip =File (zipExportPath) + val uri = FileProvider.getUriForFile(viewModel.activity, viewModel.activity.packageName + ".fileprovider", zip) + val intent = Intent(Intent.ACTION_SEND) + intent.putExtra(Intent.EXTRA_STREAM, uri) + intent.setDataAndType(uri, URLConnection.guessContentTypeFromName(zip.name)) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + val chooser = Intent.createChooser(intent, "Share logs"); + viewModel.activity.startActivity(chooser) + } else { + File(zipExportPath).delete() + } + } + } + + fun clearLogs() { + if(File(logPath).exists()){ + File(logPath).deleteRecursively() + } + + File(logPath).mkdirs() + } +} + +internal enum class LogLevel { + Debug, Stub, Info, Warning, Error, Guest, AccessLog, Notice, Trace +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt index 1c851484d..04db5d07b 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MainActivity.kt @@ -18,8 +18,10 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.anggrayudi.storage.SimpleStorageHelper +import com.halilibo.richtext.ui.RichTextThemeIntegration import org.ryujinx.android.ui.theme.RyujinxAndroidTheme import org.ryujinx.android.viewmodels.MainViewModel +import org.ryujinx.android.viewmodels.QuickSettings import org.ryujinx.android.views.MainView import kotlin.math.abs @@ -27,9 +29,11 @@ import kotlin.math.abs class MainActivity : BaseActivity() { private var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this) + private lateinit var motionSensorManager: MotionSensorManager private var _isInit: Boolean = false var isGameRunning = false var storageHelper: SimpleStorageHelper? = null + lateinit var uiHandler: UiHandler companion object { var mainViewModel: MainViewModel? = null var AppPath : String = "" @@ -64,12 +68,27 @@ class MainActivity : BaseActivity() { return val appPath: String = AppPath - val success = RyujinxNative.instance.initialize(NativeHelpers.instance.storeStringJava(appPath), false) + + var quickSettings = QuickSettings(this) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Debug.ordinal, quickSettings.enableDebugLogs) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Info.ordinal, quickSettings.enableInfoLogs) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Stub.ordinal, quickSettings.enableStubLogs) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Warning.ordinal, quickSettings.enableWarningLogs) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Error.ordinal, quickSettings.enableErrorLogs) + RyujinxNative.instance.loggingSetEnabled(LogLevel.AccessLog.ordinal, quickSettings.enableAccessLogs) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Guest.ordinal, quickSettings.enableGuestLogs) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Trace.ordinal, quickSettings.enableTraceLogs) + val success = RyujinxNative.instance.initialize(NativeHelpers.instance.storeStringJava(appPath)) + + uiHandler = UiHandler() _isInit = success } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + motionSensorManager = MotionSensorManager(this) + Thread.setDefaultUncaughtExceptionHandler(crashHandler) + if( !Environment.isExternalStorageManager() ) { @@ -78,7 +97,6 @@ class MainActivity : BaseActivity() { AppPath = this.getExternalFilesDir(null)!!.absolutePath - initialize() window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES @@ -86,16 +104,21 @@ class MainActivity : BaseActivity() { mainViewModel = MainViewModel(this) mainViewModel!!.physicalControllerManager = physicalControllerManager + mainViewModel!!.motionSensorManager = motionSensorManager + + mainViewModel!!.refreshFirmwareVersion() mainViewModel?.apply { setContent { RyujinxAndroidTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - MainView.Main(mainViewModel = this) + RichTextThemeIntegration(contentColor = { MaterialTheme.colorScheme.onSurface }) { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + MainView.Main(mainViewModel = this) + } } } } @@ -133,7 +156,7 @@ class MainActivity : BaseActivity() { fun setFullScreen(fullscreen: Boolean) { requestedOrientation = - if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER + if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER val insets = WindowCompat.getInsetsController(window, window.decorView) @@ -183,6 +206,8 @@ class MainActivity : BaseActivity() { setFullScreen(true) NativeHelpers.instance.setTurboMode(true) force60HzRefreshRate(true) + if (QuickSettings(this).enableMotion) + motionSensorManager.register() } } @@ -193,5 +218,7 @@ class MainActivity : BaseActivity() { NativeHelpers.instance.setTurboMode(false) force60HzRefreshRate(false) } + + motionSensorManager.unregister() } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MotionSensorManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MotionSensorManager.kt new file mode 100644 index 000000000..1b34c985a --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/MotionSensorManager.kt @@ -0,0 +1,121 @@ +package org.ryujinx.android + +import android.app.Activity +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener2 +import android.hardware.SensorManager +import android.view.OrientationEventListener + +class MotionSensorManager(val activity: MainActivity) : SensorEventListener2 { + private var isRegistered: Boolean = false + private var gyro: Sensor? + private var accelerometer: Sensor? + private var sensorManager: SensorManager = + activity.getSystemService(Activity.SENSOR_SERVICE) as SensorManager + private var controllerId: Int = -1 + + private val motionGyroOrientation : FloatArray = FloatArray(3) + private val motionAcelOrientation : FloatArray = FloatArray(3) + init { + accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + gyro = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + setOrientation90() + var orientationListener = object : OrientationEventListener(activity){ + override fun onOrientationChanged(orientation: Int) { + when{ + isWithinOrientationRange(orientation, 270) -> { + setOrientation270() + } + isWithinOrientationRange(orientation, 90) -> { + setOrientation90() + } + } + } + + private fun isWithinOrientationRange( + currentOrientation : Int, targetOrientation : Int, epsilon : Int = 90 + ) : Boolean { + return currentOrientation > targetOrientation - epsilon + && currentOrientation < targetOrientation + epsilon + } + } + } + + fun setOrientation270() { + motionGyroOrientation[0] = -1.0f + motionGyroOrientation[1] = 1.0f + motionGyroOrientation[2] = 1.0f + motionAcelOrientation[0] = 1.0f + motionAcelOrientation[1] = -1.0f + motionAcelOrientation[2] = -1.0f + } + fun setOrientation90() { + motionGyroOrientation[0] = 1.0f + motionGyroOrientation[1] = -1.0f + motionGyroOrientation[2] = 1.0f + motionAcelOrientation[0] = -1.0f + motionAcelOrientation[1] = 1.0f + motionAcelOrientation[2] = -1.0f + } + + fun setControllerId(id: Int){ + controllerId = id + } + + fun register(){ + if(isRegistered) + return + gyro?.apply { + sensorManager.registerListener(this@MotionSensorManager, gyro, SensorManager.SENSOR_DELAY_GAME) + } + accelerometer?.apply { + sensorManager.registerListener(this@MotionSensorManager, accelerometer, SensorManager.SENSOR_DELAY_GAME) + } + + isRegistered = true; + } + + fun unregister(){ + sensorManager.unregisterListener(this) + isRegistered = false + + if (controllerId != -1){ + RyujinxNative.instance.inputSetAccelerometerData(0.0F, 0.0F, 0.0F, controllerId) + RyujinxNative.instance.inputSetGyroData(0.0F, 0.0F, 0.0F, controllerId) + } + } + + override fun onSensorChanged(event: SensorEvent?) { + if (controllerId != -1) + if (isRegistered) + event?.apply { + when (sensor.type) { + Sensor.TYPE_ACCELEROMETER -> { + val x = motionAcelOrientation[0] * event.values[1] + val y = motionAcelOrientation[1] * event.values[0] + val z = motionAcelOrientation[2] * event.values[2] + + RyujinxNative.instance.inputSetAccelerometerData(x, y, z, controllerId) + } + + Sensor.TYPE_GYROSCOPE -> { + val x = motionGyroOrientation[0] * event.values[1] + val y = motionGyroOrientation[1] * event.values[0] + val z = motionGyroOrientation[2] * event.values[2] + RyujinxNative.instance.inputSetGyroData(x, y, z, controllerId) + } + } + } + else { + RyujinxNative.instance.inputSetAccelerometerData(0.0F, 0.0F, 0.0F, controllerId) + RyujinxNative.instance.inputSetGyroData(0.0F, 0.0F, 0.0F, controllerId) + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + } + + override fun onFlushCompleted(sensor: Sensor?) { + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt index dceab1e1e..9518435f3 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/NativeHelpers.kt @@ -31,4 +31,14 @@ class NativeHelpers { external fun getProgressValue() : Float external fun storeStringJava(string: String) : Long external fun getStringJava(id: Long) : String + external fun setIsInitialOrientationFlipped(isFlipped: Boolean) + external fun getUiHandlerRequestType() : Int + external fun getUiHandlerRequestTitle() : Long + external fun getUiHandlerRequestMessage() : Long + external fun getUiHandlerMinLength() : Int + external fun getUiHandlerMaxLength() : Int + external fun getUiHandlerKeyboardMode() : Int + external fun getUiHandlerRequestWatermark() : Long + external fun getUiHandlerRequestInitialText() : Long + external fun getUiHandlerRequestSubtitle() : Long } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt index c61ac456e..600628724 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/PhysicalControllerManager.kt @@ -1,17 +1,19 @@ package org.ryujinx.android +import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent +import org.ryujinx.android.viewmodels.QuickSettings class PhysicalControllerManager(val activity: MainActivity) { private var controllerId: Int = -1 private var ryujinxNative: RyujinxNative = RyujinxNative.instance fun onKeyEvent(event: KeyEvent) : Boolean{ - if(controllerId != -1) { - val id = getGamePadButtonInputId(event.keyCode) - - if(id != GamePadButtonInputId.None) { + val id = getGamePadButtonInputId(event.keyCode) + if(id != GamePadButtonInputId.None) { + val isNotFallback = (event.flags and KeyEvent.FLAG_FALLBACK) == 0 + if (/*controllerId != -1 &&*/ isNotFallback) { when (event.action) { KeyEvent.ACTION_UP -> { ryujinxNative.inputSetButtonReleased(id.ordinal, controllerId) @@ -23,13 +25,16 @@ class PhysicalControllerManager(val activity: MainActivity) { } return true } + else if(!isNotFallback){ + return true + } } return false } fun onMotionEvent(ev: MotionEvent) { - if(controllerId != -1) { + if(true) { if(ev.action == MotionEvent.ACTION_MOVE) { val leftStickX = ev.getAxisValue(MotionEvent.AXIS_X) val leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y) @@ -37,20 +42,60 @@ class PhysicalControllerManager(val activity: MainActivity) { val rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ) ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId) ryujinxNative.inputSetStickAxis(2, rightStickX, -rightStickY ,controllerId) + + ev.device?.apply { + if(sources and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD){ + // Controller uses HAT + val dPadHor = ev.getAxisValue(MotionEvent.AXIS_HAT_X) + val dPadVert = ev.getAxisValue(MotionEvent.AXIS_HAT_Y) + if(dPadVert == 0.0f){ + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, controllerId) + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, controllerId) + } + if(dPadHor == 0.0f){ + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, controllerId) + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, controllerId) + } + + if(dPadVert < 0.0f){ + ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, controllerId) + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, controllerId) + } + if(dPadHor < 0.0f){ + ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, controllerId) + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, controllerId) + } + + if(dPadVert > 0.0f){ + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, controllerId) + ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, controllerId) + } + if(dPadHor > 0.0f){ + ryujinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, controllerId) + ryujinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, controllerId) + } + } + } } } } - fun connect(){ + fun connect() : Int { controllerId = ryujinxNative.inputConnectGamepad(0) + return controllerId + } + + fun disconnect(){ + controllerId = -1 } private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId { + val quickSettings = QuickSettings(activity) return when (keycode) { - KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B - KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A - KeyEvent.KEYCODE_BUTTON_X -> GamePadButtonInputId.X - KeyEvent.KEYCODE_BUTTON_Y -> GamePadButtonInputId.Y + KeyEvent.KEYCODE_BUTTON_A -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.A else GamePadButtonInputId.B + KeyEvent.KEYCODE_BUTTON_B -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.B else GamePadButtonInputId.A + KeyEvent.KEYCODE_BUTTON_X -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.X else GamePadButtonInputId.Y + KeyEvent.KEYCODE_BUTTON_Y -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.Y else GamePadButtonInputId.X KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxApplication.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxApplication.kt new file mode 100644 index 000000000..69a71d5c5 --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxApplication.kt @@ -0,0 +1,19 @@ +package org.ryujinx.android + +import android.app.Application +import android.content.Context +import java.io.File + +class RyujinxApplication : Application() { + init { + instance = this + } + + fun getPublicFilesDir() : File = getExternalFilesDir(null) ?: filesDir + companion object { + lateinit var instance : RyujinxApplication + private set + + val context : Context get() = instance.applicationContext + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt index 989226de6..a01d75332 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/RyujinxNative.kt @@ -4,24 +4,28 @@ import org.ryujinx.android.viewmodels.GameInfo @Suppress("KotlinJniMissingFunction") class RyujinxNative { - external fun initialize(appPath: Long, enableDebugLogs : Boolean): Boolean + external fun initialize(appPath: Long): Boolean companion object { val instance: RyujinxNative = RyujinxNative() + init { System.loadLibrary("ryujinx") } } - external fun deviceInitialize(isHostMapped: Boolean, useNce: Boolean, - systemLanguage : Int, - regionCode : Int, - enableVsync : Boolean, - enableDockedMode : Boolean, - enablePtc : Boolean, - enableInternetAccess : Boolean, - timeZone : Long, - ignoreMissingServices : Boolean): Boolean + external fun deviceInitialize( + isHostMapped: Boolean, useNce: Boolean, + systemLanguage: Int, + regionCode: Int, + enableVsync: Boolean, + enableDockedMode: Boolean, + enablePtc: Boolean, + enableInternetAccess: Boolean, + timeZone: Long, + ignoreMissingServices: Boolean + ): Boolean + external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean external fun graphicsInitializeRenderer( extensions: Array, @@ -33,9 +37,9 @@ class RyujinxNative { external fun deviceGetGameFrameRate(): Double external fun deviceGetGameFrameTime(): Double external fun deviceGetGameFifo(): Double - external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): GameInfo + external fun deviceGetGameInfo(fileDescriptor: Int, extension: Long): GameInfo external fun deviceGetGameInfoFromPath(path: String): GameInfo - external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean + external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int, updateDescriptor: Int): Boolean external fun graphicsRendererSetSize(width: Int, height: Int) external fun graphicsRendererSetVsync(enabled: Boolean) external fun graphicsRendererRunLoop() @@ -49,19 +53,29 @@ class RyujinxNative { external fun inputSetButtonReleased(button: Int, id: Int) external fun inputConnectGamepad(index: Int): Int external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int) + external fun inputSetAccelerometerData(x: Float, y: Float, z: Float, id: Int) + external fun inputSetGyroData(x: Float, y: Float, z: Float, id: Int) external fun graphicsSetSurface(surface: Long, window: Long) external fun deviceCloseEmulation() external fun deviceSignalEmulationClose() - external fun deviceGetDlcTitleId(path: Long, ncaPath: Long) : Long - external fun deviceGetDlcContentList(path: Long, titleId: Long) : Array - external fun userGetOpenedUser() : Long - external fun userGetUserPicture(userId: Long) : Long + external fun deviceGetDlcTitleId(path: Long, ncaPath: Long): Long + external fun deviceGetDlcContentList(path: Long, titleId: Long): Array + external fun userGetOpenedUser(): Long + external fun userGetUserPicture(userId: Long): Long external fun userSetUserPicture(userId: String, picture: String) - external fun userGetUserName(userId: Long) : Long + external fun userGetUserName(userId: Long): Long external fun userSetUserName(userId: String, userName: String) - external fun userGetAllUsers() : Array + external fun userGetAllUsers(): Array external fun userAddUser(username: String, picture: String) external fun userDeleteUser(userId: String) external fun userOpenUser(userId: Long) external fun userCloseUser(userId: String) + external fun loggingSetEnabled(logLevel: Int, enabled: Boolean) + external fun deviceVerifyFirmware(fileDescriptor: Int, isXci: Boolean): Long + external fun deviceInstallFirmware(fileDescriptor: Int, isXci: Boolean) + external fun deviceGetInstalledFirmwareVersion() : Long + external fun uiHandlerSetup() + external fun uiHandlerWait() + external fun uiHandlerStopWait() + external fun uiHandlerSetResponse(isOkPressed: Boolean, input: Long) } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/UiHandler.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/UiHandler.kt new file mode 100644 index 000000000..f1b8e2cef --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/UiHandler.kt @@ -0,0 +1,215 @@ +package org.ryujinx.android + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.halilibo.richtext.markdown.Markdown +import com.halilibo.richtext.ui.RichText + +internal enum class KeyboardMode { + Default, Numeric, ASCII, FullLatin, Alphabet, SimplifiedChinese, TraditionalChinese, Korean, LanguageSet2, LanguageSet2Latin +} + +class UiHandler { + private var initialText: String = "" + private var subtitle: String = "" + private var maxLength: Int = 0 + private var minLength: Int = 0 + private var watermark: String = "" + private var type: Int = -1 + private var mode: KeyboardMode = KeyboardMode.Default + val showMessage = mutableStateOf(false) + val inputText = mutableStateOf("") + var title: String = "" + var message: String = "" + var shouldListen = true + + init { + RyujinxNative.instance.uiHandlerSetup() + } + + fun listen() { + showMessage.value = false + while (shouldListen) { + RyujinxNative.instance.uiHandlerWait() + + title = + NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestTitle()) + message = + NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestMessage()) + watermark = + NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestWatermark()) + type = NativeHelpers.instance.getUiHandlerRequestType() + minLength = NativeHelpers.instance.getUiHandlerMinLength() + maxLength = NativeHelpers.instance.getUiHandlerMaxLength() + mode = KeyboardMode.values()[NativeHelpers.instance.getUiHandlerKeyboardMode()] + subtitle = + NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestSubtitle()) + initialText = + NativeHelpers.instance.getStringJava(NativeHelpers.instance.getUiHandlerRequestInitialText()) + inputText.value = initialText + showMessage.value = type > 0 + } + } + + fun stop() { + shouldListen = false + RyujinxNative.instance.uiHandlerStopWait() + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun Compose() { + val showMessageListener = remember { + showMessage + } + + val inputListener = remember { + inputText + } + val validation = remember { + mutableStateOf("") + } + + fun validate() : Boolean{ + if(inputText.value.isEmpty()){ + validation.value = "Must be between ${minLength} and ${maxLength} characters" + } + else{ + return inputText.value.length < minLength || inputText.value.length > maxLength + } + + return false; + } + + fun getInputType(): KeyboardType { + return when(mode){ + KeyboardMode.Default -> KeyboardType.Text + KeyboardMode.Numeric -> KeyboardType.Decimal + KeyboardMode.ASCII -> KeyboardType.Ascii + else -> { KeyboardType.Text} + } + } + + fun submit() { + var input: Long = -1 + if (type == 2) { + if (inputListener.value.length < minLength || inputListener.value.length > maxLength) + return + input = + NativeHelpers.instance.storeStringJava(inputListener.value) + } + showMessageListener.value = false + RyujinxNative.instance.uiHandlerSetResponse(true, input) + } + + if (showMessageListener.value) { + AlertDialog( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + onDismissRequest = { }, + properties = DialogProperties(dismissOnBackPress = false, false) + ) { + Column { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column( + modifier = Modifier + .padding(16.dp) + ) { + Text(text = title) + Column( + modifier = Modifier + .height(128.dp) + .verticalScroll(rememberScrollState()) + .padding(8.dp), + verticalArrangement = Arrangement.Center + ) { + RichText { + Markdown(content = message) + } + if (type == 2) { + validate() + if (watermark.isNotEmpty()) + TextField( + value = inputListener.value, + onValueChange = { inputListener.value = it }, + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + label = { + Text(text = watermark) + }, + keyboardOptions = KeyboardOptions(keyboardType = getInputType()), + isError = validate() + ) + else + TextField( + value = inputListener.value, + onValueChange = { inputListener.value = it }, + modifier = Modifier + .fillMaxWidth() + .padding(4.dp), + keyboardOptions = KeyboardOptions( + keyboardType = getInputType(), + imeAction = ImeAction.Done + ), + isError = validate(), + singleLine = true, + keyboardActions = KeyboardActions(onDone = { submit() }) + ) + if (subtitle.isNotEmpty()) + Text(text = subtitle) + Text(text = validation.value) + } + } + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + ) { + Button(onClick = { + submit() + }) { + Text(text = "OK") + } + } + } + } + } + } + } + } +} + diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/providers/DocumentProvider.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/providers/DocumentProvider.kt new file mode 100644 index 000000000..dd2316a7e --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/providers/DocumentProvider.kt @@ -0,0 +1,280 @@ +package org.ryujinx.android.providers + +import android.database.Cursor +import android.database.MatrixCursor +import android.os.CancellationSignal +import android.os.ParcelFileDescriptor +import android.provider.DocumentsContract +import android.provider.DocumentsProvider +import android.webkit.MimeTypeMap +import org.ryujinx.android.BuildConfig +import org.ryujinx.android.R +import org.ryujinx.android.RyujinxApplication +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException + +class DocumentProvider : DocumentsProvider() { + private val baseDirectory = File(RyujinxApplication.instance.getPublicFilesDir().canonicalPath) + private val applicationName = "Ryujinx" + + companion object { + private val DEFAULT_ROOT_PROJECTION : Array = arrayOf( + DocumentsContract.Root.COLUMN_ROOT_ID, + DocumentsContract.Root.COLUMN_MIME_TYPES, + DocumentsContract.Root.COLUMN_FLAGS, + DocumentsContract.Root.COLUMN_ICON, + DocumentsContract.Root.COLUMN_TITLE, + DocumentsContract.Root.COLUMN_SUMMARY, + DocumentsContract.Root.COLUMN_DOCUMENT_ID, + DocumentsContract.Root.COLUMN_AVAILABLE_BYTES + ) + + private val DEFAULT_DOCUMENT_PROJECTION : Array = arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_LAST_MODIFIED, + DocumentsContract.Document.COLUMN_FLAGS, + DocumentsContract.Document.COLUMN_SIZE + ) + + const val AUTHORITY : String = BuildConfig.APPLICATION_ID + ".providers" + + const val ROOT_ID : String = "root" + } + + override fun onCreate() : Boolean { + return true + } + + /** + * @return The [File] that corresponds to the document ID supplied by [getDocumentId] + */ + private fun getFile(documentId : String) : File { + if (documentId.startsWith(ROOT_ID)) { + val file = baseDirectory.resolve(documentId.drop(ROOT_ID.length + 1)) + if (!file.exists()) throw FileNotFoundException("${file.absolutePath} ($documentId) not found") + return file + } else { + throw FileNotFoundException("'$documentId' is not in any known root") + } + } + + /** + * @return A unique ID for the provided [File] + */ + private fun getDocumentId(file : File) : String { + return "$ROOT_ID/${file.toRelativeString(baseDirectory)}" + } + + override fun queryRoots(projection : Array?) : Cursor { + val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION) + + cursor.newRow().apply { + add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID) + add(DocumentsContract.Root.COLUMN_SUMMARY, null) + add(DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD) + add(DocumentsContract.Root.COLUMN_TITLE, applicationName) + add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory)) + add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*") + add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory.freeSpace) + add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher_foreground) + } + + return cursor + } + + override fun queryDocument(documentId : String?, projection : Array?) : Cursor { + val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION) + return includeFile(cursor, documentId, null) + } + + override fun isChildDocument(parentDocumentId : String?, documentId : String?) : Boolean { + return documentId?.startsWith(parentDocumentId!!) ?: false + } + + /** + * @return A new [File] with a unique name based off the supplied [name], not conflicting with any existing file + */ + fun File.resolveWithoutConflict(name : String) : File { + var file = resolve(name) + if (file.exists()) { + var noConflictId = 1 // Makes sure two files don't have the same name by adding a number to the end + val extension = name.substringAfterLast('.') + val baseName = name.substringBeforeLast('.') + while (file.exists()) + file = resolve("$baseName (${noConflictId++}).$extension") + } + return file + } + + override fun createDocument(parentDocumentId : String?, mimeType : String?, displayName : String) : String? { + val parentFile = getFile(parentDocumentId!!) + val newFile = parentFile.resolveWithoutConflict(displayName) + + try { + if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) { + if (!newFile.mkdir()) + throw IOException("Failed to create directory") + } else { + if (!newFile.createNewFile()) + throw IOException("Failed to create file") + } + } catch (e : IOException) { + throw FileNotFoundException("Couldn't create document '${newFile.path}': ${e.message}") + } + + return getDocumentId(newFile) + } + + override fun deleteDocument(documentId : String?) { + val file = getFile(documentId!!) + if (!file.delete()) + throw FileNotFoundException("Couldn't delete document with ID '$documentId'") + } + + override fun removeDocument(documentId : String, parentDocumentId : String?) { + val parent = getFile(parentDocumentId!!) + val file = getFile(documentId) + + if (parent == file || file.parentFile == null || file.parentFile!! == parent) { + if (!file.delete()) + throw FileNotFoundException("Couldn't delete document with ID '$documentId'") + } else { + throw FileNotFoundException("Couldn't delete document with ID '$documentId'") + } + } + + override fun renameDocument(documentId : String?, displayName : String?) : String? { + if (displayName == null) + throw FileNotFoundException("Couldn't rename document '$documentId' as the new name is null") + + val sourceFile = getFile(documentId!!) + val sourceParentFile = sourceFile.parentFile ?: throw FileNotFoundException("Couldn't rename document '$documentId' as it has no parent") + val destFile = sourceParentFile.resolve(displayName) + + try { + if (!sourceFile.renameTo(destFile)) + throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'") + } catch (e : Exception) { + throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': ${e.message}") + } + + return getDocumentId(destFile) + } + + private fun copyDocument( + sourceDocumentId : String, sourceParentDocumentId : String, + targetParentDocumentId : String? + ) : String? { + if (!isChildDocument(sourceParentDocumentId, sourceDocumentId)) + throw FileNotFoundException("Couldn't copy document '$sourceDocumentId' as its parent is not '$sourceParentDocumentId'") + + return copyDocument(sourceDocumentId, targetParentDocumentId) + } + + override fun copyDocument(sourceDocumentId : String, targetParentDocumentId : String?) : String? { + val parent = getFile(targetParentDocumentId!!) + val oldFile = getFile(sourceDocumentId) + val newFile = parent.resolveWithoutConflict(oldFile.name) + + try { + if (!(newFile.createNewFile() && newFile.setWritable(true) && newFile.setReadable(true))) + throw IOException("Couldn't create new file") + + FileInputStream(oldFile).use { inStream -> + FileOutputStream(newFile).use { outStream -> + inStream.copyTo(outStream) + } + } + } catch (e : IOException) { + throw FileNotFoundException("Couldn't copy document '$sourceDocumentId': ${e.message}") + } + + return getDocumentId(newFile) + } + + override fun moveDocument( + sourceDocumentId : String, sourceParentDocumentId : String?, + targetParentDocumentId : String? + ) : String? { + try { + val newDocumentId = copyDocument( + sourceDocumentId, sourceParentDocumentId!!, + targetParentDocumentId + ) + removeDocument(sourceDocumentId, sourceParentDocumentId) + return newDocumentId + } catch (e : FileNotFoundException) { + throw FileNotFoundException("Couldn't move document '$sourceDocumentId'") + } + } + + private fun includeFile(cursor : MatrixCursor, documentId : String?, file : File?) : MatrixCursor { + val localDocumentId = documentId ?: file?.let { getDocumentId(it) } + val localFile = file ?: getFile(documentId!!) + + var flags = 0 + if (localFile.isDirectory && localFile.canWrite()) { + flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE + } else if (localFile.canWrite()) { + flags = DocumentsContract.Document.FLAG_SUPPORTS_WRITE + flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE + + flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE + flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_MOVE + flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_COPY + flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME + } + + cursor.newRow().apply { + add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId) + add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, if (localFile == baseDirectory) applicationName else localFile.name) + add(DocumentsContract.Document.COLUMN_SIZE, localFile.length()) + add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile)) + add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified()) + add(DocumentsContract.Document.COLUMN_FLAGS, flags) + if (localFile == baseDirectory) + add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher_foreground) + } + + return cursor + } + + private fun getTypeForFile(file : File) : Any? { + return if (file.isDirectory) + DocumentsContract.Document.MIME_TYPE_DIR + else + getTypeForName(file.name) + } + + private fun getTypeForName(name : String) : Any? { + val lastDot = name.lastIndexOf('.') + if (lastDot >= 0) { + val extension = name.substring(lastDot + 1) + val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + if (mime != null) + return mime + } + return "application/octect-stream" + } + + override fun queryChildDocuments(parentDocumentId : String?, projection : Array?, sortOrder : String?) : Cursor { + var cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION) + + val parent = getFile(parentDocumentId!!) + for (file in parent.listFiles()!!) + cursor = includeFile(cursor, null, file) + + return cursor + } + + override fun openDocument(documentId : String?, mode : String?, signal : CancellationSignal?) : ParcelFileDescriptor { + val file = documentId?.let { getFile(it) } + val accessMode = ParcelFileDescriptor.parseMode(mode) + return ParcelFileDescriptor.open(file, accessMode) + } +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt index a61eadf49..7d95ec594 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/DlcViewModel.kt @@ -40,7 +40,7 @@ class DlcViewModel(val titleId: String) { val path = file.getAbsolutePath(storageHelper.storage.context) if (path.isNotEmpty()) { data?.apply { - var contents = RyujinxNative.instance.deviceGetDlcContentList( + val contents = RyujinxNative.instance.deviceGetDlcContentList( NativeHelpers.instance.storeStringJava(path), titleId.toLong(16) ) diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt index 376ae33a8..017809ea0 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/GameModel.kt @@ -1,13 +1,17 @@ package org.ryujinx.android.viewmodels import android.content.Context +import android.net.Uri import android.os.ParcelFileDescriptor import androidx.documentfile.provider.DocumentFile import com.anggrayudi.storage.file.extension +import org.ryujinx.android.NativeHelpers import org.ryujinx.android.RyujinxNative class GameModel(var file: DocumentFile, val context: Context) { + private var updateDescriptor: ParcelFileDescriptor? = null + var type: FileType var descriptor: ParcelFileDescriptor? = null var fileName: String? var fileSize = 0.0 @@ -19,8 +23,9 @@ class GameModel(var file: DocumentFile, val context: Context) { init { fileName = file.name - var pid = open() - val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, file.extension.contains("xci")) + val pid = open() + val ext = NativeHelpers.instance.storeStringJava(file.extension) + val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, ext) close() fileSize = gameInfo.FileSize @@ -29,6 +34,16 @@ class GameModel(var file: DocumentFile, val context: Context) { developer = gameInfo.Developer version = gameInfo.Version icon = gameInfo.Icon + type = when { + (file.extension == "xci") -> FileType.Xci + (file.extension == "nsp") -> FileType.Nsp + (file.extension == "nro") -> FileType.Nro + else -> FileType.None + } + + if (type == FileType.Nro && (titleName.isNullOrEmpty() || titleName == "Unknown")) { + titleName = file.name + } } fun open() : Int { @@ -37,13 +52,29 @@ class GameModel(var file: DocumentFile, val context: Context) { return descriptor?.fd ?: 0 } + fun openUpdate() : Int { + if(titleId?.isNotEmpty() == true) { + val vm = TitleUpdateViewModel(titleId ?: "") + + if(vm.data?.selected?.isNotEmpty() == true){ + val uri = Uri.parse(vm.data?.selected) + val file = DocumentFile.fromSingleUri(context, uri) + if(file?.exists() == true){ + updateDescriptor = context.contentResolver.openFileDescriptor(file.uri, "rw") + + return updateDescriptor ?.fd ?: -1; + } + } + } + + return -1; + } + fun close() { descriptor?.close() descriptor = null - } - - fun isXci() : Boolean { - return file.extension == "xci" + updateDescriptor?.close() + updateDescriptor = null } } @@ -55,3 +86,10 @@ class GameInfo { var Version: String? = null var Icon: String? = null } + +enum class FileType{ + None, + Nsp, + Xci, + Nro +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt index 2419dbb37..f98b66fb4 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/HomeViewModel.kt @@ -6,19 +6,20 @@ import androidx.documentfile.provider.DocumentFile import androidx.preference.PreferenceManager import com.anggrayudi.storage.file.DocumentFileCompat import com.anggrayudi.storage.file.DocumentFileType -import com.anggrayudi.storage.file.FileFullPath import com.anggrayudi.storage.file.extension -import com.anggrayudi.storage.file.getAbsolutePath import com.anggrayudi.storage.file.search import org.ryujinx.android.MainActivity +import java.util.Locale import kotlin.concurrent.thread class HomeViewModel( val activity: MainActivity? = null, val mainViewModel: MainViewModel? = null ) { + private var shouldReload: Boolean = false + private var savedFolder: String = "" private var isLoading: Boolean = false - private var loadedCache: List = listOf() + private var loadedCache: MutableList = mutableListOf() private var gameFolderPath: DocumentFile? = null private var sharedPref: SharedPreferences? = null val gameList: SnapshotStateList = SnapshotStateList() @@ -26,46 +27,34 @@ class HomeViewModel( init { if (activity != null) { sharedPref = PreferenceManager.getDefaultSharedPreferences(activity) - activity.storageHelper!!.onFolderSelected = { requestCode, folder -> - run { - gameFolderPath = folder - val p = folder.getAbsolutePath(activity!!) - val editor = sharedPref?.edit() - editor?.putString("gameFolder", p) - editor?.apply() - reloadGameList() - } - } - - val savedFolder = sharedPref?.getString("gameFolder", "") ?: "" - - if (savedFolder.isNotEmpty()) { - try { - gameFolderPath = DocumentFileCompat.fromFullPath( - activity, - savedFolder, - documentType = DocumentFileType.FOLDER, - requiresWriteAccess = true - ) - - reloadGameList() - } catch (e: Exception) { - - } - } } } - fun openGameFolder() { - val path = sharedPref?.getString("gameFolder", "") ?: "" + fun ensureReloadIfNecessary() { + val oldFolder = savedFolder + savedFolder = sharedPref?.getString("gameFolder", "") ?: "" - if (path.isEmpty()) - activity?.storageHelper?.storage?.openFolderPicker() - else - activity?.storageHelper?.storage?.openFolderPicker( - activity.storageHelper!!.storage.requestCodeFolderPicker, - FileFullPath(activity, path) + if (savedFolder.isNotEmpty() && (shouldReload || savedFolder != oldFolder)) { + gameFolderPath = DocumentFileCompat.fromFullPath( + mainViewModel?.activity!!, + savedFolder, + documentType = DocumentFileType.FOLDER, + requiresWriteAccess = true ) + + reloadGameList() + } + } + + fun filter(query : String){ + gameList.clear() + gameList.addAll(loadedCache.filter { it.titleName != null && it.titleName!!.isNotEmpty() && (query.trim() + .isEmpty() || it.titleName!!.lowercase(Locale.getDefault()) + .contains(query)) }) + } + + fun requestReload(){ + shouldReload = true } fun reloadGameList() { @@ -80,26 +69,24 @@ class HomeViewModel( isLoading = true thread { try { + loadedCache.clear() val files = mutableListOf() for (file in folder.search(false, DocumentFileType.FILE)) { - if (file.extension == "xci" || file.extension == "nsp") + if (file.extension == "xci" || file.extension == "nsp" || file.extension == "nro") activity.let { val item = GameModel(file, it) - files.add(item) - gameList.add(item) + + if(item.titleId?.isNotEmpty() == true && item.titleName?.isNotEmpty() == true) { + loadedCache.add(item) + gameList.add(item) + } } } - loadedCache = files.toList() - isLoading = false } finally { isLoading = false } } } - - fun clearLoadedCache(){ - loadedCache = listOf() - } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt index 7f451449d..bf538287c 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/MainViewModel.kt @@ -12,7 +12,9 @@ import kotlinx.coroutines.sync.Semaphore import org.ryujinx.android.GameController import org.ryujinx.android.GameHost import org.ryujinx.android.GraphicsConfiguration +import org.ryujinx.android.Logging import org.ryujinx.android.MainActivity +import org.ryujinx.android.MotionSensorManager import org.ryujinx.android.NativeGraphicsInterop import org.ryujinx.android.NativeHelpers import org.ryujinx.android.PerformanceManager @@ -25,12 +27,15 @@ import java.io.File @SuppressLint("WrongConstant") class MainViewModel(val activity: MainActivity) { var physicalControllerManager: PhysicalControllerManager? = null + var motionSensorManager: MotionSensorManager? = null var gameModel: GameModel? = null var controller: GameController? = null var performanceManager: PerformanceManager? = null var selected: GameModel? = null var isMiiEditorLaunched = false val userViewModel = UserViewModel() + val logging = Logging(this) + var firmwareVersion = "" private var gameTimeState: MutableState? = null private var gameFpsState: MutableState? = null private var fifoState: MutableState? = null @@ -38,6 +43,7 @@ class MainViewModel(val activity: MainActivity) { private var progressValue: MutableState? = null private var showLoading: MutableState? = null private var refreshUser: MutableState? = null + var gameHost: GameHost? = null set(value) { field = value @@ -59,6 +65,16 @@ class MainViewModel(val activity: MainActivity) { RyujinxNative.instance.deviceSignalEmulationClose() gameHost?.close() RyujinxNative.instance.deviceCloseEmulation() + motionSensorManager?.unregister() + physicalControllerManager?.disconnect() + motionSensorManager?.setControllerId(-1) + } + + fun refreshFirmwareVersion(){ + var handle = RyujinxNative.instance.deviceGetInstalledFirmwareVersion() + if(handle != -1L) { + firmwareVersion = NativeHelpers.instance.getStringJava(handle) + } } fun loadGame(game:GameModel) : Boolean { @@ -69,6 +85,8 @@ class MainViewModel(val activity: MainActivity) { if (descriptor == 0) return false + val update = game.openUpdate() + gameModel = game isMiiEditorLaunched = false @@ -162,7 +180,7 @@ class MainViewModel(val activity: MainActivity) { if (!success) return false - success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.isXci()) + success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.type.ordinal, update) if (!success) return false @@ -170,8 +188,6 @@ class MainViewModel(val activity: MainActivity) { return true } - - fun loadMiiEditor() : Boolean { val nativeRyujinx = RyujinxNative.instance @@ -351,6 +367,8 @@ class MainViewModel(val activity: MainActivity) { activity.setFullScreen(true) navController?.navigate("game") activity.isGameRunning = true + if (QuickSettings(activity).enableMotion) + motionSensorManager?.register() } fun setProgressStates( diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt index 18300f52b..c608b7f7f 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/QuickSettings.kt @@ -15,6 +15,19 @@ class QuickSettings(val activity: Activity) { var enableShaderCache: Boolean var enableTextureRecompression: Boolean var resScale : Float + var isGrid : Boolean + var useSwitchLayout : Boolean + var enableMotion : Boolean + + // Logs + var enableDebugLogs: Boolean + var enableStubLogs: Boolean + var enableInfoLogs: Boolean + var enableWarningLogs: Boolean + var enableErrorLogs: Boolean + var enableGuestLogs: Boolean + var enableAccessLogs: Boolean + var enableTraceLogs: Boolean private var sharedPref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) @@ -29,5 +42,46 @@ class QuickSettings(val activity: Activity) { enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false) resScale = sharedPref.getFloat("resScale", 1f) useVirtualController = sharedPref.getBoolean("useVirtualController", true) + isGrid = sharedPref.getBoolean("isGrid", true) + useSwitchLayout = sharedPref.getBoolean("useSwitchLayout", true) + enableMotion = sharedPref.getBoolean("enableMotion", true) + + enableDebugLogs = sharedPref.getBoolean("enableDebugLogs", false) + enableStubLogs = sharedPref.getBoolean("enableStubLogs", false) + enableInfoLogs = sharedPref.getBoolean("enableInfoLogs", true) + enableWarningLogs = sharedPref.getBoolean("enableWarningLogs", true) + enableErrorLogs = sharedPref.getBoolean("enableErrorLogs", true) + enableGuestLogs = sharedPref.getBoolean("enableGuestLogs", true) + enableAccessLogs = sharedPref.getBoolean("enableAccessLogs", false) + enableTraceLogs = sharedPref.getBoolean("enableStubLogs", false) + } + + fun save(){ + val editor = sharedPref.edit() + + editor.putBoolean("isHostMapped", isHostMapped) + editor.putBoolean("useNce", useNce) + editor.putBoolean("enableVsync", enableVsync) + editor.putBoolean("enableDocked", enableDocked) + editor.putBoolean("enablePtc", enablePtc) + editor.putBoolean("ignoreMissingServices", ignoreMissingServices) + editor.putBoolean("enableShaderCache", enableShaderCache) + editor.putBoolean("enableTextureRecompression", enableTextureRecompression) + editor.putFloat("resScale", resScale) + editor.putBoolean("useVirtualController", useVirtualController) + editor.putBoolean("isGrid", isGrid) + editor.putBoolean("useSwitchLayout", useSwitchLayout) + editor.putBoolean("enableMotion", enableMotion) + + editor.putBoolean("enableDebugLogs", enableDebugLogs) + editor.putBoolean("enableStubLogs", enableStubLogs) + editor.putBoolean("enableInfoLogs", enableInfoLogs) + editor.putBoolean("enableWarningLogs", enableWarningLogs) + editor.putBoolean("enableErrorLogs", enableErrorLogs) + editor.putBoolean("enableGuestLogs", enableGuestLogs) + editor.putBoolean("enableAccessLogs", enableAccessLogs) + editor.putBoolean("enableTraceLogs", enableTraceLogs) + + editor.apply() } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt index 5db75c5f9..f83cb7076 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/SettingsViewModel.kt @@ -2,18 +2,43 @@ package org.ryujinx.android.viewmodels import android.content.SharedPreferences import androidx.compose.runtime.MutableState +import androidx.documentfile.provider.DocumentFile import androidx.navigation.NavHostController import androidx.preference.PreferenceManager +import com.anggrayudi.storage.callback.FileCallback +import com.anggrayudi.storage.file.FileFullPath +import com.anggrayudi.storage.file.copyFileTo +import com.anggrayudi.storage.file.extension +import com.anggrayudi.storage.file.getAbsolutePath +import org.ryujinx.android.LogLevel import org.ryujinx.android.MainActivity +import org.ryujinx.android.NativeHelpers +import org.ryujinx.android.RyujinxNative +import java.io.File +import kotlin.concurrent.thread class SettingsViewModel(var navController: NavHostController, val activity: MainActivity) { + var selectedFirmwareVersion: String = "" + private var previousFileCallback: ((requestCode: Int, files: List) -> Unit)? + private var previousFolderCallback: ((requestCode: Int, folder: DocumentFile) -> Unit)? private var sharedPref: SharedPreferences + var selectedFirmwareFile: DocumentFile? = null init { sharedPref = getPreferences() + previousFolderCallback = activity.storageHelper!!.onFolderSelected + previousFileCallback = activity.storageHelper!!.onFileSelected + activity.storageHelper!!.onFolderSelected = { requestCode, folder -> + run { + val p = folder.getAbsolutePath(activity) + val editor = sharedPref.edit() + editor?.putString("gameFolder", p) + editor?.apply() + } + } } - private fun getPreferences() : SharedPreferences { + private fun getPreferences(): SharedPreferences { return PreferenceManager.getDefaultSharedPreferences(activity) } @@ -27,9 +52,19 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main enableShaderCache: MutableState, enableTextureRecompression: MutableState, resScale: MutableState, - useVirtualController: MutableState - ) - { + useVirtualController: MutableState, + isGrid: MutableState, + useSwitchLayout: MutableState, + enableMotion: MutableState, + enableDebugLogs: MutableState, + enableStubLogs: MutableState, + enableInfoLogs: MutableState, + enableWarningLogs: MutableState, + enableErrorLogs: MutableState, + enableGuestLogs: MutableState, + enableAccessLogs: MutableState, + enableTraceLogs: MutableState + ) { isHostMapped.value = sharedPref.getBoolean("isHostMapped", true) useNce.value = sharedPref.getBoolean("useNce", true) @@ -38,9 +73,22 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main enablePtc.value = sharedPref.getBoolean("enablePtc", true) ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false) enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true) - enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false) + enableTextureRecompression.value = + sharedPref.getBoolean("enableTextureRecompression", false) resScale.value = sharedPref.getFloat("resScale", 1f) useVirtualController.value = sharedPref.getBoolean("useVirtualController", true) + isGrid.value = sharedPref.getBoolean("isGrid", true) + useSwitchLayout.value = sharedPref.getBoolean("useSwitchLayout", true) + enableMotion.value = sharedPref.getBoolean("enableMotion", true) + + enableDebugLogs.value = sharedPref.getBoolean("enableDebugLogs", false) + enableStubLogs.value = sharedPref.getBoolean("enableStubLogs", false) + enableInfoLogs.value = sharedPref.getBoolean("enableInfoLogs", true) + enableWarningLogs.value = sharedPref.getBoolean("enableWarningLogs", true) + enableErrorLogs.value = sharedPref.getBoolean("enableErrorLogs", true) + enableGuestLogs.value = sharedPref.getBoolean("enableGuestLogs", true) + enableAccessLogs.value = sharedPref.getBoolean("enableAccessLogs", false) + enableTraceLogs.value = sharedPref.getBoolean("enableStubLogs", false) } fun save( @@ -53,8 +101,19 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main enableShaderCache: MutableState, enableTextureRecompression: MutableState, resScale: MutableState, - useVirtualController: MutableState - ){ + useVirtualController: MutableState, + isGrid: MutableState, + useSwitchLayout: MutableState, + enableMotion: MutableState, + enableDebugLogs: MutableState, + enableStubLogs: MutableState, + enableInfoLogs: MutableState, + enableWarningLogs: MutableState, + enableErrorLogs: MutableState, + enableGuestLogs: MutableState, + enableAccessLogs: MutableState, + enableTraceLogs: MutableState + ) { val editor = sharedPref.edit() editor.putBoolean("isHostMapped", isHostMapped.value) @@ -67,7 +126,148 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value) editor.putFloat("resScale", resScale.value) editor.putBoolean("useVirtualController", useVirtualController.value) + editor.putBoolean("isGrid", isGrid.value) + editor.putBoolean("useSwitchLayout", useSwitchLayout.value) + editor.putBoolean("enableMotion", enableMotion.value) + + editor.putBoolean("enableDebugLogs", enableDebugLogs.value) + editor.putBoolean("enableStubLogs", enableStubLogs.value) + editor.putBoolean("enableInfoLogs", enableInfoLogs.value) + editor.putBoolean("enableWarningLogs", enableWarningLogs.value) + editor.putBoolean("enableErrorLogs", enableErrorLogs.value) + editor.putBoolean("enableGuestLogs", enableGuestLogs.value) + editor.putBoolean("enableAccessLogs", enableAccessLogs.value) + editor.putBoolean("enableTraceLogs", enableTraceLogs.value) editor.apply() + activity.storageHelper!!.onFolderSelected = previousFolderCallback + + RyujinxNative.instance.loggingSetEnabled(LogLevel.Debug.ordinal, enableDebugLogs.value) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Info.ordinal, enableInfoLogs.value) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Stub.ordinal, enableStubLogs.value) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Warning.ordinal, enableWarningLogs.value) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Error.ordinal, enableErrorLogs.value) + RyujinxNative.instance.loggingSetEnabled(LogLevel.AccessLog.ordinal, enableAccessLogs.value) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Guest.ordinal, enableGuestLogs.value) + RyujinxNative.instance.loggingSetEnabled(LogLevel.Trace.ordinal, enableTraceLogs.value) } -} \ No newline at end of file + + fun openGameFolder() { + val path = sharedPref?.getString("gameFolder", "") ?: "" + + if (path.isEmpty()) + activity.storageHelper?.storage?.openFolderPicker() + else + activity.storageHelper?.storage?.openFolderPicker( + activity.storageHelper!!.storage.requestCodeFolderPicker, + FileFullPath(activity, path) + ) + } + + fun importProdKeys() { + activity.storageHelper!!.onFileSelected = { requestCode, files -> + run { + activity.storageHelper!!.onFileSelected = previousFileCallback + val file = files.firstOrNull() + file?.apply { + if (name == "prod.keys") { + val outputFile = File(MainActivity.AppPath + "/system"); + outputFile.delete() + + thread { + file.copyFileTo( + activity, + outputFile, + callback = object : FileCallback() { + override fun onCompleted(result: Any) { + super.onCompleted(result) + } + }) + } + } + } + } + } + activity.storageHelper?.storage?.openFilePicker() + } + + fun selectFirmware(installState: MutableState) { + if (installState.value != FirmwareInstallState.None) + return + activity.storageHelper!!.onFileSelected = { _, files -> + run { + activity.storageHelper!!.onFileSelected = previousFileCallback + val file = files.firstOrNull() + file?.apply { + if (extension == "xci" || extension == "zip") { + installState.value = FirmwareInstallState.Verifying + thread { + val descriptor = + activity.contentResolver.openFileDescriptor(file.uri, "rw") + descriptor?.use { d -> + val version = RyujinxNative.instance.deviceVerifyFirmware( + d.fd, + extension == "xci" + ) + selectedFirmwareFile = file + if (version != -1L) { + selectedFirmwareVersion = + NativeHelpers.instance.getStringJava(version) + installState.value = FirmwareInstallState.Query + } else { + installState.value = FirmwareInstallState.Cancelled + } + } + } + } else { + installState.value = FirmwareInstallState.Cancelled + } + } + } + } + activity.storageHelper?.storage?.openFilePicker() + } + + fun installFirmware(installState: MutableState) { + if (installState.value != FirmwareInstallState.Query) + return + if (selectedFirmwareFile == null) { + installState.value = FirmwareInstallState.None + return + } + selectedFirmwareFile?.apply { + val descriptor = + activity.contentResolver.openFileDescriptor(uri, "rw") + descriptor?.use { d -> + installState.value = FirmwareInstallState.Install + thread { + try { + RyujinxNative.instance.deviceInstallFirmware( + d.fd, + extension == "xci" + ) + } finally { + MainActivity.mainViewModel?.refreshFirmwareVersion() + installState.value = FirmwareInstallState.Done + } + } + } + } + } + + fun clearFirmwareSelection(installState: MutableState){ + selectedFirmwareFile = null + selectedFirmwareVersion = "" + installState.value = FirmwareInstallState.None + } +} + + +enum class FirmwareInstallState{ + None, + Cancelled, + Verifying, + Query, + Install, + Done +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt index e42108220..2f3df1b08 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt @@ -1,12 +1,15 @@ package org.ryujinx.android.viewmodels +import android.content.Intent +import android.net.Uri import androidx.compose.runtime.MutableState import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.toLowerCase +import androidx.documentfile.provider.DocumentFile import com.anggrayudi.storage.SimpleStorageHelper +import com.anggrayudi.storage.file.extension import com.google.gson.Gson -import org.ryujinx.android.Helpers import org.ryujinx.android.MainActivity import java.io.File import kotlin.math.max @@ -16,29 +19,32 @@ class TitleUpdateViewModel(val titleId: String) { private var basePath: String private var updateJsonName = "updates.json" private var storageHelper: SimpleStorageHelper + var currentPaths: MutableList = mutableListOf() var pathsState: SnapshotStateList? = null companion object { const val UpdateRequestCode = 1002 } - fun Remove(index: Int) { + fun remove(index: Int) { if (index <= 0) return data?.paths?.apply { - val removed = removeAt(index - 1) - File(removed).deleteRecursively() + val str = removeAt(index - 1) + Uri.parse(str)?.apply { + storageHelper.storage.context.contentResolver.releasePersistableUriPermission( + this, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } pathsState?.clear() pathsState?.addAll(this) + currentPaths = this } } - fun Add( - isCopying: MutableState, - copyProgress: MutableState, - currentProgressName: MutableState - ) { + fun add() { val callBack = storageHelper.onFileSelected storageHelper.onFileSelected = { requestCode, files -> @@ -47,29 +53,29 @@ class TitleUpdateViewModel(val titleId: String) { if (requestCode == UpdateRequestCode) { val file = files.firstOrNull() file?.apply { - // Copy updates to internal data folder - val updatePath = "$basePath/update" - File(updatePath).mkdirs() - Helpers.copyToData( - this, - updatePath, - storageHelper, - isCopying, - copyProgress, - currentProgressName, ::refreshPaths - ) + if(file.extension == "nsp"){ + storageHelper.storage.context.contentResolver.takePersistableUriPermission(file.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + currentPaths.add(file.uri.toString()) + } } + + refreshPaths() } } } storageHelper.openFilePicker(UpdateRequestCode) } - fun refreshPaths() { + private fun refreshPaths() { data?.apply { - val updatePath = "$basePath/update" val existingPaths = mutableListOf() - File(updatePath).listFiles()?.forEach { existingPaths.add(it.absolutePath) } + currentPaths.forEach { + val uri = Uri.parse(it) + val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri) + if(file?.exists() == true){ + existingPaths.add(it) + } + } if (!existingPaths.contains(selected)) { selected = "" @@ -88,7 +94,6 @@ class TitleUpdateViewModel(val titleId: String) { openDialog: MutableState ) { data?.apply { - val updatePath = "$basePath/update" this.selected = "" if (paths.isNotEmpty() && index > 0) { val ind = max(index - 1, paths.count() - 1) @@ -98,18 +103,29 @@ class TitleUpdateViewModel(val titleId: String) { File(basePath).mkdirs() - var metadata = TitleUpdateMetadata() + val metadata = TitleUpdateMetadata() val savedUpdates = mutableListOf() - File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) } + currentPaths.forEach { + val uri = Uri.parse(it) + val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri) + if(file?.exists() == true){ + savedUpdates.add(it) + } + } metadata.paths = savedUpdates - val selectedName = File(selected).name - val newSelectedPath = "$updatePath/$selectedName" - if (File(newSelectedPath).exists()) { - metadata.selected = newSelectedPath + if(selected.isNotEmpty()){ + val uri = Uri.parse(selected) + val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri) + if(file?.exists() == true){ + metadata.selected = selected + } + } + else { + metadata.selected = selected } - var json = gson.toJson(metadata) + val json = gson.toJson(metadata) File("$basePath/$updateJsonName").writeText(json) openDialog.value = false @@ -137,10 +153,13 @@ class TitleUpdateViewModel(val titleId: String) { val gson = Gson() data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java) - refreshPaths() } - + currentPaths = data?.paths ?: mutableListOf() storageHelper = MainActivity.StorageHelper!! + refreshPaths() + + File("$basePath/update").deleteRecursively() + } } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt index ea14f654b..ab442f93d 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/DlcViews.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon @@ -51,25 +50,13 @@ class DlcViews { Column(modifier = Modifier.padding(16.dp)) { Column { - Row(modifier = Modifier.padding(8.dp) + Row(modifier = Modifier + .padding(8.dp) .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text(text = "DLC for ${name}", textAlign = TextAlign.Center, modifier = Modifier.align( Alignment.CenterVertically )) - IconButton( - onClick = { - viewModel.add(refresh) - }, - modifier = Modifier.align( - Alignment.CenterVertically - ) - ) { - Icon( - Icons.Filled.Add, - contentDescription = "Add" - ) - } } Surface( modifier = Modifier @@ -119,16 +106,27 @@ class DlcViews { } Spacer(modifier = Modifier.height(8.dp)) - TextButton( - modifier = Modifier.align(Alignment.End), - onClick = { - openDialog.value = false - viewModel.save(dlcList) - }, - ) { - Text("Save") + Row(modifier = Modifier.align(Alignment.End)) { + TextButton( + modifier = Modifier.padding(4.dp), + onClick = { + viewModel.add(refresh) + } + ) { + + Text("Add") + } + TextButton( + modifier = Modifier.padding(4.dp), + onClick = { + openDialog.value = false + viewModel.save(dlcList) + }, + ) { + Text("Save") + } } } } } -} \ No newline at end of file +} diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt index 63bd49209..2b320dc6f 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/GameViews.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -82,6 +83,9 @@ class GameViews { val enableVsync = remember { mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync) } + val enableMotion = remember { + mutableStateOf(QuickSettings(mainViewModel.activity).enableMotion) + } val showMore = remember { mutableStateOf(false) } @@ -169,27 +173,56 @@ class GameViews { modifier = Modifier.padding(16.dp), shape = MaterialTheme.shapes.medium ) { - Row(modifier = Modifier.padding(8.dp)) { - IconButton(modifier = Modifier.padding(4.dp), onClick = { - showMore.value = false - showController.value = !showController.value - mainViewModel.controller?.setVisible(showController.value) - }) { - Icon( - imageVector = Icons.videoGame(), - contentDescription = "Toggle Virtual Pad" + Column { + Row( + modifier = Modifier, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Motion", + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(end = 16.dp) ) + Switch(checked = enableMotion.value, onCheckedChange = { + showMore.value = false + enableMotion.value = !enableMotion.value + val settings = QuickSettings(mainViewModel.activity) + settings.enableMotion = enableMotion.value + settings.save() + if (enableMotion.value) + mainViewModel.motionSensorManager?.register() + else + mainViewModel.motionSensorManager?.unregister() + }) } - IconButton(modifier = Modifier.padding(4.dp), onClick = { - showMore.value = false - enableVsync.value = !enableVsync.value - RyujinxNative.instance.graphicsRendererSetVsync(enableVsync.value) - }) { - Icon( - imageVector = Icons.vSync(), - tint = if (enableVsync.value) Color.Green else Color.Red, - contentDescription = "Toggle VSync" - ) + Row(modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween) { + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = false + showController.value = !showController.value + ryujinxNative.inputReleaseTouchPoint() + mainViewModel.controller?.setVisible(showController.value) + }) { + Icon( + imageVector = Icons.videoGame(), + contentDescription = "Toggle Virtual Pad" + ) + } + IconButton(modifier = Modifier.padding(4.dp), onClick = { + showMore.value = false + enableVsync.value = !enableVsync.value + RyujinxNative.instance.graphicsRendererSetVsync( + enableVsync.value + ) + }) { + Icon( + imageVector = Icons.vSync(), + tint = if (enableVsync.value) Color.Green else Color.Red, + contentDescription = "Toggle VSync" + ) + } } } } @@ -284,6 +317,8 @@ class GameViews { } } } + + mainViewModel.activity.uiHandler.Compose() } } @@ -301,7 +336,7 @@ class GameViews { Surface( modifier = Modifier.padding(16.dp), - color = MaterialTheme.colorScheme.surface.copy(0.4f) + color = MaterialTheme.colorScheme.background.copy(0.4f) ) { Column { var gameTimeVal = 0.0 diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt index 877aa8cb7..275ab369f 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/HomeViews.kt @@ -4,6 +4,7 @@ import android.content.res.Resources import android.graphics.BitmapFactory import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -18,8 +19,12 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Menu @@ -28,18 +33,15 @@ import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.BottomAppBarDefaults import androidx.compose.material3.Card import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults @@ -50,16 +52,22 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.anggrayudi.storage.extension.launchOnUiThread +import org.ryujinx.android.R +import org.ryujinx.android.viewmodels.FileType import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.HomeViewModel +import org.ryujinx.android.viewmodels.QuickSettings import java.util.Base64 import java.util.Locale import kotlin.concurrent.thread @@ -67,7 +75,8 @@ import kotlin.math.roundToInt class HomeViews { companion object { - const val ImageSize = 150 + const val ListImageSize = 150 + const val GridImageSize = 300 @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -75,11 +84,15 @@ class HomeViews { viewModel: HomeViewModel = HomeViewModel(), navController: NavHostController? = null ) { + viewModel.ensureReloadIfNecessary() val showAppActions = remember { mutableStateOf(false) } val showLoading = remember { mutableStateOf(false) } val openTitleUpdateDialog = remember { mutableStateOf(false) } val canClose = remember { mutableStateOf(true) } val openDlcDialog = remember { mutableStateOf(false) } + val selectedModel = remember { + mutableStateOf(viewModel.mainViewModel?.selected) + } val query = remember { mutableStateOf("") } @@ -153,154 +166,59 @@ class HomeViews { } } ) - }, - bottomBar = { - BottomAppBar( - actions = { - if (showAppActions.value) { - IconButton(onClick = { - if(viewModel.mainViewModel?.selected != null) { - thread { - showLoading.value = true - val success = - viewModel.mainViewModel?.loadGame(viewModel.mainViewModel.selected!!) - ?: false - if (success) { - launchOnUiThread { - viewModel.mainViewModel?.navigateToGame() - } - } else { - viewModel.mainViewModel?.selected!!.close() - } - showLoading.value = false - } - } - }) { - Icon( - org.ryujinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface), - contentDescription = "Run" - ) - } - val showAppMenu = remember { mutableStateOf(false) } - Box { - IconButton(onClick = { - showAppMenu.value = true - }) { - Icon( - Icons.Filled.Menu, - contentDescription = "Menu" - ) - } - DropdownMenu( - expanded = showAppMenu.value, - onDismissRequest = { showAppMenu.value = false }) { - DropdownMenuItem(text = { - Text(text = "Clear PPTC Cache") - }, onClick = { - showAppMenu.value = false - viewModel.mainViewModel?.clearPptcCache( - viewModel.mainViewModel?.selected?.titleId ?: "" - ) - }) - DropdownMenuItem(text = { - Text(text = "Purge Shader Cache") - }, onClick = { - showAppMenu.value = false - viewModel.mainViewModel?.purgeShaderCache( - viewModel.mainViewModel?.selected?.titleId ?: "" - ) - }) - DropdownMenuItem(text = { - Text(text = "Manage Updates") - }, onClick = { - showAppMenu.value = false - openTitleUpdateDialog.value = true - }) - DropdownMenuItem(text = { - Text(text = "Manage DLC") - }, onClick = { - showAppMenu.value = false - openDlcDialog.value = true - }) - } - } - } - - /*\val showAppletMenu = remember { mutableStateOf(false) } - Box { - IconButton(onClick = { - showAppletMenu.value = true - }) { - Icon( - org.ryujinx.android.Icons.applets(MaterialTheme.colorScheme.onSurface), - contentDescription = "Applets" - ) - } - DropdownMenu( - expanded = showAppletMenu.value, - onDismissRequest = { showAppletMenu.value = false }) { - DropdownMenuItem(text = { - Text(text = "Launch Mii Editor") - }, onClick = { - showAppletMenu.value = false - showLoading.value = true - thread { - val success = - viewModel.mainViewModel?.loadMiiEditor() ?: false - if (success) { - launchOnUiThread { - viewModel.mainViewModel?.navigateToGame() - } - } else - viewModel.mainViewModel!!.isMiiEditorLaunched = false - showLoading.value = false - } - }) - } - }*/ - }, - floatingActionButton = { - FloatingActionButton( - onClick = { - viewModel.openGameFolder() - }, - containerColor = BottomAppBarDefaults.bottomAppBarFabColor, - elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation() - ) { - Icon( - org.ryujinx.android.Icons.folderOpen(MaterialTheme.colorScheme.onSurface), - contentDescription = "Open Folder" - ) - } - } - ) } - ) { contentPadding -> Box(modifier = Modifier.padding(contentPadding)) { val list = remember { viewModel.gameList } - val selectedModel = remember { - mutableStateOf(viewModel.mainViewModel?.selected) - } - LazyColumn(Modifier.fillMaxSize()) { - items(list) { - it.titleName?.apply { - if (this.isNotEmpty() && (query.value.trim() - .isEmpty() || this.lowercase( - Locale.getDefault() + viewModel.filter(query.value) + var settings = QuickSettings(viewModel.activity!!) + + if (settings.isGrid) { + val size = GridImageSize / Resources.getSystem().displayMetrics.density + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = (size + 4).dp), + modifier = Modifier + .fillMaxSize() + .padding(4.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + items(list) { + it.titleName?.apply { + if (this.isNotEmpty() && (query.value.trim() + .isEmpty() || this.lowercase(Locale.getDefault()) + .contains(query.value)) ) - .contains(query.value)) - ) - GameItem( - it, - viewModel, - showAppActions, - showLoading, - selectedModel + GridGameItem( + it, + viewModel, + showAppActions, + showLoading, + selectedModel + ) + } + } + } + } else { + LazyColumn(Modifier.fillMaxSize()) { + items(list) { + it.titleName?.apply { + if (this.isNotEmpty() && (query.value.trim() + .isEmpty() || this.lowercase( + Locale.getDefault() + ) + .contains(query.value)) ) + ListGameItem( + it, + viewModel, + showAppActions, + showLoading, + selectedModel, + ) + } } } } @@ -368,11 +286,94 @@ class HomeViews { } } } + + if (showAppActions.value) + ModalBottomSheet( + content = { + Row( + modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + if (showAppActions.value) { + IconButton(onClick = { + if (viewModel.mainViewModel?.selected != null) { + thread { + showLoading.value = true + val success = + viewModel.mainViewModel?.loadGame(viewModel.mainViewModel.selected!!) + ?: false + if (success) { + launchOnUiThread { + viewModel.mainViewModel?.navigateToGame() + } + } else { + viewModel.mainViewModel?.selected!!.close() + } + showLoading.value = false + } + } + }) { + Icon( + org.ryujinx.android.Icons.playArrow(MaterialTheme.colorScheme.onSurface), + contentDescription = "Run" + ) + } + val showAppMenu = remember { mutableStateOf(false) } + Box { + IconButton(onClick = { + showAppMenu.value = true + }) { + Icon( + Icons.Filled.Menu, + contentDescription = "Menu" + ) + } + DropdownMenu( + expanded = showAppMenu.value, + onDismissRequest = { showAppMenu.value = false }) { + DropdownMenuItem(text = { + Text(text = "Clear PPTC Cache") + }, onClick = { + showAppMenu.value = false + viewModel.mainViewModel?.clearPptcCache( + viewModel.mainViewModel?.selected?.titleId ?: "" + ) + }) + DropdownMenuItem(text = { + Text(text = "Purge Shader Cache") + }, onClick = { + showAppMenu.value = false + viewModel.mainViewModel?.purgeShaderCache( + viewModel.mainViewModel?.selected?.titleId ?: "" + ) + }) + DropdownMenuItem(text = { + Text(text = "Manage Updates") + }, onClick = { + showAppMenu.value = false + openTitleUpdateDialog.value = true + }) + DropdownMenuItem(text = { + Text(text = "Manage DLC") + }, onClick = { + showAppMenu.value = false + openDlcDialog.value = true + }) + } + } + } + } + }, + onDismissRequest = { + showAppActions.value = false + selectedModel.value = null + } + ) } @OptIn(ExperimentalFoundationApi::class) @Composable - fun GameItem( + fun ListGameItem( gameModel: GameModel, viewModel: HomeViewModel, showAppActions: MutableState, @@ -400,7 +401,7 @@ class HomeViews { selected = null } selectedModel.value = null - } else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") { + } else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) { thread { showLoading.value = true val success = @@ -429,10 +430,11 @@ class HomeViews { horizontalArrangement = Arrangement.SpaceBetween ) { Row { - if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") { + if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) { if (gameModel.icon?.isNotEmpty() == true) { val pic = decoder.decode(gameModel.icon) - val size = ImageSize / Resources.getSystem().displayMetrics.density + val size = + ListImageSize / Resources.getSystem().displayMetrics.density Image( bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) .asImageBitmap(), @@ -442,7 +444,9 @@ class HomeViews { .width(size.roundToInt().dp) .height(size.roundToInt().dp) ) - } else NotAvailableIcon() + } else if (gameModel.type == FileType.Nro) + NROIcon() + else NotAvailableIcon() } else NotAvailableIcon() Column { Text(text = gameModel.titleName ?: "") @@ -458,12 +462,107 @@ class HomeViews { } } + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun GridGameItem( + gameModel: GameModel, + viewModel: HomeViewModel, + showAppActions: MutableState, + showLoading: MutableState, + selectedModel: MutableState + ) { + remember { + selectedModel + } + val color = + if (selectedModel.value == gameModel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface + + val decoder = Base64.getDecoder() + Surface( + shape = MaterialTheme.shapes.medium, + color = color, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .combinedClickable( + onClick = { + if (viewModel.mainViewModel?.selected != null) { + showAppActions.value = false + viewModel.mainViewModel?.apply { + selected = null + } + selectedModel.value = null + } else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) { + thread { + showLoading.value = true + val success = + viewModel.mainViewModel?.loadGame(gameModel) ?: false + if (success) { + launchOnUiThread { + viewModel.mainViewModel?.navigateToGame() + } + } else { + gameModel.close() + } + showLoading.value = false + } + } + }, + onLongClick = { + viewModel.mainViewModel?.selected = gameModel + showAppActions.value = true + selectedModel.value = gameModel + }) + ) { + Column(modifier = Modifier.padding(4.dp)) { + if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) { + if (gameModel.icon?.isNotEmpty() == true) { + val pic = decoder.decode(gameModel.icon) + val size = GridImageSize / Resources.getSystem().displayMetrics.density + Image( + bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) + .asImageBitmap(), + contentDescription = gameModel.titleName + " icon", + modifier = Modifier + .padding(0.dp) + .clip(RoundedCornerShape(16.dp)) + .align(Alignment.CenterHorizontally) + ) + } else if (gameModel.type == FileType.Nro) + NROIcon() + else NotAvailableIcon() + } else NotAvailableIcon() + Text( + text = gameModel.titleName ?: "N/A", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(vertical = 4.dp) + .basicMarquee() + ) + } + } + } + @Composable fun NotAvailableIcon() { - val size = ImageSize / Resources.getSystem().displayMetrics.density + val size = ListImageSize / Resources.getSystem().displayMetrics.density Icon( Icons.Filled.Add, - contentDescription = "Options", + contentDescription = "N/A", + modifier = Modifier + .padding(end = 8.dp) + .width(size.roundToInt().dp) + .height(size.roundToInt().dp) + ) + } + + @Composable + fun NROIcon() { + val size = ListImageSize / Resources.getSystem().displayMetrics.density + Image( + painter = painterResource(id = R.drawable.icon_nro), + contentDescription = "NRO", modifier = Modifier .padding(end = 8.dp) .width(size.roundToInt().dp) diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt index d1c17d4b6..2e6434739 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/SettingViews.kt @@ -1,6 +1,9 @@ package org.ryujinx.android.views import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Intent +import android.provider.DocumentsContract import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState @@ -14,6 +17,8 @@ import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -56,6 +61,8 @@ import androidx.documentfile.provider.DocumentFile import com.anggrayudi.storage.file.extension import org.ryujinx.android.Helpers import org.ryujinx.android.MainActivity +import org.ryujinx.android.providers.DocumentProvider +import org.ryujinx.android.viewmodels.FirmwareInstallState import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.SettingsViewModel import org.ryujinx.android.viewmodels.VulkanDriverViewModel @@ -66,7 +73,7 @@ class SettingViews { const val EXPANSTION_TRANSITION_DURATION = 450 const val IMPORT_CODE = 12341 - @OptIn(ExperimentalMaterial3Api::class) + @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun Main(settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel) { val loaded = remember { @@ -103,6 +110,27 @@ class SettingViews { val useVirtualController = remember { mutableStateOf(true) } + val showFirwmareDialog = remember { + mutableStateOf(false) + } + val firmwareInstallState = remember { + mutableStateOf(FirmwareInstallState.None) + } + val firmwareVersion = remember { + mutableStateOf(mainViewModel.firmwareVersion) + } + val isGrid = remember { mutableStateOf(true) } + val useSwitchLayout = remember { mutableStateOf(true) } + val enableMotion = remember { mutableStateOf(true) } + + val enableDebugLogs = remember { mutableStateOf(true) } + val enableStubLogs = remember { mutableStateOf(true) } + val enableInfoLogs = remember { mutableStateOf(true) } + val enableWarningLogs = remember { mutableStateOf(true) } + val enableErrorLogs = remember { mutableStateOf(true) } + val enableGuestLogs = remember { mutableStateOf(true) } + val enableAccessLogs = remember { mutableStateOf(true) } + val enableTraceLogs = remember { mutableStateOf(true) } if (!loaded.value) { settingsViewModel.initializeState( @@ -112,7 +140,18 @@ class SettingViews { enableShaderCache, enableTextureRecompression, resScale, - useVirtualController + useVirtualController, + isGrid, + useSwitchLayout, + enableMotion, + enableDebugLogs, + enableStubLogs, + enableInfoLogs, + enableWarningLogs, + enableErrorLogs, + enableGuestLogs, + enableAccessLogs, + enableTraceLogs ) loaded.value = true } @@ -134,7 +173,18 @@ class SettingViews { enableShaderCache, enableTextureRecompression, resScale, - useVirtualController + useVirtualController, + isGrid, + useSwitchLayout, + enableMotion, + enableDebugLogs, + enableStubLogs, + enableInfoLogs, + enableWarningLogs, + enableErrorLogs, + enableGuestLogs, + enableAccessLogs, + enableTraceLogs ) settingsViewModel.navController.popBackStack() }) { @@ -145,6 +195,223 @@ class SettingViews { Column(modifier = Modifier .padding(contentPadding) .verticalScroll(rememberScrollState())) { + ExpandableView(onCardArrowClick = { }, title = "App") { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Use Grid", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = isGrid.value, onCheckedChange = { + isGrid.value = !isGrid.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Game Folder", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Button(onClick = { + settingsViewModel.openGameFolder() + }) { + Text(text = "Choose Folder") + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "System Firmware", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Text( + text = firmwareVersion.value, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Button(onClick = { + fun createIntent(action: String): Intent { + val intent = Intent(action) + intent.addCategory(Intent.CATEGORY_DEFAULT) + intent.data = DocumentsContract.buildRootUri( + DocumentProvider.AUTHORITY, + DocumentProvider.ROOT_ID + ) + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + return intent + } + try { + mainViewModel.activity.startActivity(createIntent(Intent.ACTION_VIEW)) + return@Button + } catch (_: ActivityNotFoundException) { + } + try { + mainViewModel.activity.startActivity(createIntent("android.provider.action.BROWSE")) + return@Button + } catch (_: ActivityNotFoundException) { + } + try { + mainViewModel.activity.startActivity(createIntent("com.google.android.documentsui")) + return@Button + } catch (_: ActivityNotFoundException) { + } + try { + mainViewModel.activity.startActivity(createIntent("com.android.documentsui")) + return@Button + } catch (_: ActivityNotFoundException) { + } + }) { + Text(text = "Open App Folder") + } + + Button(onClick = { + settingsViewModel.importProdKeys() + }) { + Text(text = "Import prod Keys") + } + + Button(onClick = { + showFirwmareDialog.value = true + }) { + Text(text = "Install Firmware") + } + } + } + } + + if(showFirwmareDialog.value) { + AlertDialog(onDismissRequest = { + if(firmwareInstallState.value != FirmwareInstallState.Install) { + showFirwmareDialog.value = false + settingsViewModel.clearFirmwareSelection(firmwareInstallState) + } + }) { + Card( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + verticalArrangement = Arrangement.SpaceBetween + ) { + if (firmwareInstallState.value == FirmwareInstallState.None) { + Text(text = "Select a zip or XCI file to install from.") + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth() + .padding(top = 4.dp) + ) { + Button(onClick = { + settingsViewModel.selectFirmware( + firmwareInstallState + ) + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "Select File") + } + Button(onClick = { + showFirwmareDialog.value = false + settingsViewModel.clearFirmwareSelection( + firmwareInstallState + ) + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "Cancel") + } + } + } else if (firmwareInstallState.value == FirmwareInstallState.Query) { + Text(text = "Firmware ${settingsViewModel.selectedFirmwareVersion} will be installed. Do you want to continue?") + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth() + .padding(top = 4.dp) + ) { + Button(onClick = { + settingsViewModel.installFirmware( + firmwareInstallState + ) + + if(firmwareInstallState.value == FirmwareInstallState.None){ + showFirwmareDialog.value = false + settingsViewModel.clearFirmwareSelection(firmwareInstallState) + } + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "Yes") + } + Button(onClick = { + showFirwmareDialog.value = false + settingsViewModel.clearFirmwareSelection( + firmwareInstallState + ) + }, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(text = "No") + } + } + } else if (firmwareInstallState.value == FirmwareInstallState.Install) { + Text(text = "Installing Firmware ${settingsViewModel.selectedFirmwareVersion}...") + LinearProgressIndicator(modifier = Modifier + .padding(top = 4.dp)) + } else if (firmwareInstallState.value == FirmwareInstallState.Verifying) { + Text(text = "Verifying selected file...") + LinearProgressIndicator(modifier = Modifier + .fillMaxWidth() + ) + } + else if (firmwareInstallState.value == FirmwareInstallState.Done) { + Text(text = "Installed Firmware ${settingsViewModel.selectedFirmwareVersion}") + firmwareVersion.value = mainViewModel.firmwareVersion + } + else if(firmwareInstallState.value == FirmwareInstallState.Cancelled){ + val file = settingsViewModel.selectedFirmwareFile + if(file != null){ + if(file.extension == "xci" || file.extension == "zip"){ + if(settingsViewModel.selectedFirmwareVersion.isEmpty()) { + Text(text = "Unable to find version in selected file") + } + else { + Text(text = "Unknown Error has occurred. Please check logs") + } + } + else { + Text(text = "File type is not supported") + } + } + else { + Text(text = "File type is not supported") + } + } + } + } + } + } ExpandableView(onCardArrowClick = { }, title = "System") { Column(modifier = Modifier.fillMaxWidth()) { Row( @@ -330,15 +597,17 @@ class SettingViews { showImportCompletion.value = false importFile.value = null mainViewModel.userViewModel.refreshUsers() - mainViewModel.homeViewModel.clearLoadedCache() + mainViewModel.homeViewModel.requestReload() }) { Card( modifier = Modifier, shape = MaterialTheme.shapes.medium ) { - Text(modifier = Modifier - .padding(24.dp), - text = "App Data import completed.") + Text( + modifier = Modifier + .padding(24.dp), + text = "App Data import completed." + ) } } } @@ -591,6 +860,173 @@ class SettingViews { useVirtualController.value = !useVirtualController.value }) } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Motion", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableMotion.value, onCheckedChange = { + enableMotion.value = !enableMotion.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Use Switch Controller Layout", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = useSwitchLayout.value, onCheckedChange = { + useSwitchLayout.value = !useSwitchLayout.value + }) + } + } + } + ExpandableView(onCardArrowClick = { }, title = "Log") { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Debug Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableDebugLogs.value, onCheckedChange = { + enableDebugLogs.value = !enableDebugLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Stub Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableStubLogs.value, onCheckedChange = { + enableStubLogs.value = !enableStubLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Info Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableInfoLogs.value, onCheckedChange = { + enableInfoLogs.value = !enableInfoLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Warning Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableWarningLogs.value, onCheckedChange = { + enableWarningLogs.value = !enableWarningLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Error Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableErrorLogs.value, onCheckedChange = { + enableErrorLogs.value = !enableErrorLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Guest Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableGuestLogs.value, onCheckedChange = { + enableGuestLogs.value = !enableGuestLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Access Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableAccessLogs.value, onCheckedChange = { + enableAccessLogs.value = !enableAccessLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Enable Trace Logs", + modifier = Modifier.align(Alignment.CenterVertically) + ) + Switch(checked = enableTraceLogs.value, onCheckedChange = { + enableTraceLogs.value = !enableTraceLogs.value + }) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Button(onClick = { + mainViewModel.logging.requestExport() + }) { + Text(text = "Send Logs") + } + } } } } @@ -602,7 +1038,18 @@ class SettingViews { enableShaderCache, enableTextureRecompression, resScale, - useVirtualController + useVirtualController, + isGrid, + useSwitchLayout, + enableMotion, + enableDebugLogs, + enableStubLogs, + enableInfoLogs, + enableWarningLogs, + enableErrorLogs, + enableGuestLogs, + enableAccessLogs, + enableTraceLogs ) settingsViewModel.navController.popBackStack() } diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt index 719911db1..7eb8d62cf 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt @@ -1,5 +1,6 @@ package org.ryujinx.android.views +import android.net.Uri import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -13,7 +14,6 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.Surface @@ -28,13 +28,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.documentfile.provider.DocumentFile +import org.ryujinx.android.MainActivity import org.ryujinx.android.viewmodels.TitleUpdateViewModel -import java.io.File class TitleUpdateViews { companion object { @Composable - fun Main(titleId: String, name: String, openDialog: MutableState, canClose: MutableState) { + fun Main( + titleId: String, + name: String, + openDialog: MutableState, + canClose: MutableState + ) { val viewModel = TitleUpdateViewModel(titleId) val selected = remember { mutableStateOf(0) } @@ -43,15 +49,6 @@ class TitleUpdateViews { } Column(modifier = Modifier.padding(16.dp)) { - val isCopying = remember { - mutableStateOf(false) - } - val copyProgress = remember { - mutableStateOf(0.0f) - } - var currentProgressName = remember { - mutableStateOf("Starting Copy") - } Column { Text(text = "Updates for ${name}", textAlign = TextAlign.Center) Surface( @@ -88,18 +85,24 @@ class TitleUpdateViews { var index = 1 for (path in paths) { val i = index - Row(modifier = Modifier.padding(8.dp)) { - RadioButton( - selected = (selected.value == i), - onClick = { selected.value = i }) - Text( - text = File(path).name, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterVertically) - ) + val uri = Uri.parse(path) + val file = DocumentFile.fromSingleUri( + MainActivity.mainViewModel!!.activity, + uri + ) + file?.apply { + Row(modifier = Modifier.padding(8.dp)) { + RadioButton( + selected = (selected.value == i), + onClick = { selected.value = i }) + Text( + text = file.name ?: "", + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterVertically) + ) + } } - index++ } } @@ -107,7 +110,7 @@ class TitleUpdateViews { Row(modifier = Modifier.align(Alignment.End)) { IconButton( onClick = { - viewModel.Remove(selected.value) + viewModel.remove(selected.value) } ) { Icon( @@ -118,7 +121,7 @@ class TitleUpdateViews { IconButton( onClick = { - viewModel.Add(isCopying, copyProgress, currentProgressName) + viewModel.add() } ) { Icon( @@ -129,33 +132,12 @@ class TitleUpdateViews { } } - if (isCopying.value) { - Text(text = "Copying updates to local storage") - Text(text = currentProgressName.value) - Row { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - progress = copyProgress.value - ) - TextButton( - onClick = { - isCopying.value = false - canClose.value = true - viewModel.refreshPaths() - }, - ) { - Text("Cancel") - } - } - } Spacer(modifier = Modifier.height(18.dp)) TextButton( modifier = Modifier.align(Alignment.End), onClick = { - if (!isCopying.value) { - canClose.value = true - viewModel.save(selected.value, openDialog) - } + canClose.value = true + viewModel.save(selected.value, openDialog) }, ) { Text("Save") diff --git a/src/RyujinxAndroid/app/src/main/res/drawable/icon_nro.png b/src/RyujinxAndroid/app/src/main/res/drawable/icon_nro.png new file mode 100644 index 000000000..3a9da6218 Binary files /dev/null and b/src/RyujinxAndroid/app/src/main/res/drawable/icon_nro.png differ diff --git a/src/RyujinxAndroid/app/src/main/res/xml/provider_paths.xml b/src/RyujinxAndroid/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 000000000..cfbfdb5bb --- /dev/null +++ b/src/RyujinxAndroid/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/RyujinxAndroid/gradle.properties b/src/RyujinxAndroid/gradle.properties index 9a5caf10c..cb15bc25e 100644 --- a/src/RyujinxAndroid/gradle.properties +++ b/src/RyujinxAndroid/gradle.properties @@ -24,7 +24,7 @@ android.nonTransitiveRClass=true # Build configuration # It needs to be set to either "debug" or "release" and can also be overriden on a per build basis # by adding -Dorg.ryujinx.config=NAME to the command line. -org.ryujinx.config=debug +org.ryujinx.config=release # Controls stripping of symbols from libryujinx # Setting this property to auto causes symbols to be stripped for release builds, # but not for debug builds. @@ -33,3 +33,4 @@ org.ryujinx.config=debug org.ryujinx.symbols.strip=auto # Output path of libryujinx.so org.ryujinx.publish.path=app/src/main/jniLibs/arm64-v8a +org.ryujinx.llvm.toolchain.path=C\:\\Android\\android-sdk\\ndk\\25.1.8937393\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin