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