1
0
forked from MeloNX/MeloNX

android - add grid list option

android - adjust grid view design, remove bottom app bar

android - reload list if game folder changed, fix game updates scanning

android - set nativeaot instruction set support

android - bump version

android - bump version

android - add log export, providers to browse app data

android - add log settings

android - add button to open ryujinx app folder

android - allow sensor to change orientation during emulation

android - add support for nro

android - add motion support

android - implement firmware installation

android -  ensure controller respects users controller visibility settings at launch

android - fix settings app action buttons. fix dlc manager add button missing

android - add hack to fix orientation issue

android - fix stick showing as dpad

android - set controller event as handled

android - add option to swap button layouts to nintendo style

android - add basic software keyboard support

android - add option to disable motion

android - remote developer name from grid items

android - fix dpad input on generic android controllers

android - move title updates support to SAF

android - change game stats background color
This commit is contained in:
Emmanuel Hansen 2023-12-30 12:35:38 +00:00
parent eac63c756e
commit 28df6f1c4e
36 changed files with 2607 additions and 440 deletions

View File

@ -1,4 +1,4 @@
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Logging.Formatters; using Ryujinx.Common.Logging.Formatters;
using Ryujinx.Common.Logging.Targets; using Ryujinx.Common.Logging.Targets;
using System; using System;
@ -12,7 +12,7 @@ namespace LibRyujinx
string ILogTarget.Name { get => _name; } string ILogTarget.Name { get => _name; }
public AndroidLogTarget( string name) public AndroidLogTarget(string name)
{ {
_name = name; _name = name;
_formatter = new DefaultLogFormatter(); _formatter = new DefaultLogFormatter();

View File

@ -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();
}
}
}

View File

@ -1,4 +1,4 @@
using LibRyujinx.Jni; using LibRyujinx.Jni;
using LibRyujinx.Jni.Pointers; using LibRyujinx.Jni.Pointers;
using LibRyujinx.Jni.Primitives; using LibRyujinx.Jni.Primitives;
using LibRyujinx.Jni.References; using LibRyujinx.Jni.References;
@ -42,11 +42,36 @@ namespace LibRyujinx
private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch); private extern static JStringLocalRef createString(JEnvRef jEnv, IntPtr ch);
[DllImport("libryujinxjni")] [DllImport("libryujinxjni")]
private extern static long storeString(string ch); internal extern static long storeString(string ch);
[DllImport("libryujinxjni")]
private extern static IntPtr getString(long id);
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); var pointer = getString(id);
if (pointer != IntPtr.Zero) if (pointer != IntPtr.Zero)
@ -84,21 +109,21 @@ namespace LibRyujinx
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_initialize")] [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"); Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
PlatformInfo.IsBionic = true; PlatformInfo.IsBionic = true;
Logger.AddTarget( Logger.AddTarget(
new AsyncLogTargetWrapper( new AsyncLogTargetWrapper(
new AndroidLogTarget("Ryujinx"), new AndroidLogTarget("RyujinxLog"),
1000, 1000,
AsyncLogTargetOverflowAction.Block AsyncLogTargetOverflowAction.Block
)); ));
var path = GetStoredString(jpathId); var path = GetStoredString(jpathId);
var init = Initialize(path, enableDebugLogs); var init = Initialize(path);
_surfaceEvent?.Set(); _surfaceEvent?.Set();
@ -237,7 +262,7 @@ namespace LibRyujinx
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")] [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"); Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
if (SwitchDevice?.EmulationContext == null) if (SwitchDevice?.EmulationContext == null)
@ -246,8 +271,62 @@ namespace LibRyujinx
} }
var stream = OpenFile(descriptor); 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")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
@ -413,6 +492,13 @@ namespace LibRyujinx
RunLoop(); 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")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfoFromPath")]
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef path) public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JStringLocalRef path)
{ {
@ -422,12 +508,12 @@ namespace LibRyujinx
} }
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")] [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"); Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
using var stream = OpenFile(fileDescriptor); using var stream = OpenFile(fileDescriptor);
var ext = GetStoredString(extension);
var info = GetGameInfo(stream, isXci); var info = GetGameInfo(stream, ext.ToLower());
return GetInfo(jEnv, info); return GetInfo(jEnv, info);
} }
@ -546,6 +632,20 @@ namespace LibRyujinx
SetButtonReleased((GamepadButtonInputId)(int)button, id); 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")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_inputSetStickAxis")]
public static void JniSetStickAxis(JEnvRef jEnv, JObjectLocalRef jObj, JInt stick, JFloat x, JFloat y, JInt id) public static void JniSetStickAxis(JEnvRef jEnv, JObjectLocalRef jObj, JInt stick, JFloat x, JFloat y, JInt id)
{ {
@ -635,6 +735,30 @@ namespace LibRyujinx
DeleteUser(userId); 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")] [UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_userOpenUser")]
public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JLong userIdPtr) public static void JniOpenUser(JEnvRef jEnv, JObjectLocalRef jObj, JLong userIdPtr)
{ {

View File

@ -11,8 +11,8 @@ android {
applicationId "org.ryujinx.android" applicationId "org.ryujinx.android"
minSdk 30 minSdk 30
targetSdk 33 targetSdk 33
versionCode 10004 versionCode 10010
versionName '1.0.4' versionName '1.0.10'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -49,6 +49,7 @@ android {
buildFeatures { buildFeatures {
compose true compose true
prefab true prefab true
buildConfig true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion '1.3.2' kotlinCompilerExtensionVersion '1.3.2'

View File

@ -17,6 +17,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:name=".RyujinxApplication"
android:allowBackup="true" android:allowBackup="true"
android:appCategory="game" android:appCategory="game"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@ -39,6 +40,26 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="org.ryujinx.android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name=".providers.DocumentProvider"
android:authorities="org.ryujinx.android.providers"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application> </application>
</manifest> </manifest>

View File

@ -38,6 +38,36 @@
void* _ryujinxNative = NULL; 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 // Ryujinx imported functions
bool (*initialize)(char*) = NULL; bool (*initialize)(char*) = NULL;
@ -46,7 +76,7 @@ long _currentRenderingThreadId = 0;
JavaVM* _vm = nullptr; JavaVM* _vm = nullptr;
jobject _mainActivity = nullptr; jobject _mainActivity = nullptr;
jclass _mainActivityClass = nullptr; jclass _mainActivityClass = nullptr;
std::string _currentString = "";
string_helper str_helper = string_helper(); string_helper str_helper = string_helper();
UiHandler ui_handler = UiHandler();
#endif //RYUJINXNATIVE_RYUIJNX_H #endif //RYUJINXNATIVE_RYUIJNX_H

View File

@ -185,6 +185,8 @@ void setProgressInfo(char* info, float progressValue) {
progress = progressValue; progress = progressValue;
} }
bool isInitialOrientationFlipped = true;
extern "C" extern "C"
void setCurrentTransform(long native_window, int transform){ void setCurrentTransform(long native_window, int transform){
if(native_window == 0 || native_window == -1) if(native_window == 0 || native_window == -1)
@ -201,10 +203,10 @@ void setCurrentTransform(long native_window, int transform){
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY; nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY;
break; break;
case 0x2: case 0x2:
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_90; nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_90;
break; break;
case 0x4: case 0x4:
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_180; nativeTransform = isInitialOrientationFlipped ? ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_IDENTITY : ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_180;
break; break;
case 0x8: case 0x8:
nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_270; nativeTransform = ANativeWindowTransform::ANATIVEWINDOW_TRANSFORM_ROTATE_270;
@ -325,6 +327,37 @@ const char* getString(long id){
return cstr; 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" extern "C"
JNIEXPORT jlong JNICALL JNIEXPORT jlong JNICALL
Java_org_ryujinx_android_NativeHelpers_storeStringJava(JNIEnv *env, jobject thiz, jstring string) { 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" extern "C"
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_org_ryujinx_android_NativeHelpers_getStringJava(JNIEnv *env, jobject thiz, jlong id) { 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;
} }

View File

@ -1,16 +1,9 @@
package org.ryujinx.android package org.ryujinx.android
import android.os.Bundle
import android.os.PersistableBundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
abstract class BaseActivity : ComponentActivity() { abstract class BaseActivity : ComponentActivity() {
companion object{ companion object{
val crashHandler = CrashHandler() val crashHandler = CrashHandler()
} }
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
super.onCreate(savedInstanceState, persistentState)
}
} }

View File

@ -8,6 +8,6 @@ class CrashHandler : UncaughtExceptionHandler {
override fun uncaughtException(t: Thread, e: Throwable) { override fun uncaughtException(t: Thread, e: Throwable) {
crashLog += e.toString() + "\n" 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)
} }
} }

View File

@ -11,8 +11,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.swordfish.radialgamepad.library.RadialGamePad import com.swordfish.radialgamepad.library.RadialGamePad
import com.swordfish.radialgamepad.library.config.ButtonConfig import com.swordfish.radialgamepad.library.config.ButtonConfig
@ -29,6 +27,7 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.QuickSettings
typealias GamePad = RadialGamePad typealias GamePad = RadialGamePad
typealias GamePadConfig = RadialGamePadConfig typealias GamePadConfig = RadialGamePadConfig
@ -65,6 +64,7 @@ class GameController(var activity: Activity) {
} }
controller.controllerView = c controller.controllerView = c
viewModel.setGameController(controller) viewModel.setGameController(controller)
controller.setVisible(QuickSettings(viewModel.activity).useVirtualController)
c c
}) })
} }

View File

@ -8,7 +8,6 @@ import android.view.SurfaceView
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import org.ryujinx.android.viewmodels.GameModel import org.ryujinx.android.viewmodels.GameModel
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.QuickSettings
import kotlin.concurrent.thread import kotlin.concurrent.thread
@SuppressLint("ViewConstructor") @SuppressLint("ViewConstructor")
@ -76,6 +75,8 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
_isInit = false _isInit = false
_isStarted = false _isStarted = false
mainViewModel.activity.uiHandler.stop()
_updateThread?.join() _updateThread?.join()
_renderingThreadWatcher?.join() _renderingThreadWatcher?.join()
} }
@ -88,21 +89,16 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
_nativeRyujinx.inputInitialize(width, height) _nativeRyujinx.inputInitialize(width, height)
val settings = QuickSettings(mainViewModel.activity) val id = mainViewModel.physicalControllerManager?.connect()
mainViewModel.motionSensorManager?.setControllerId(id ?: -1)
if (!settings.useVirtualController) {
mainViewModel.controller?.setVisible(false)
} else {
mainViewModel.controller?.connect()
}
mainViewModel.physicalControllerManager?.connect()
_nativeRyujinx.graphicsRendererSetSize( _nativeRyujinx.graphicsRendererSetSize(
surfaceHolder.surfaceFrame.width(), surfaceHolder.surfaceFrame.width(),
surfaceHolder.surfaceFrame.height() surfaceHolder.surfaceFrame.height()
) )
NativeHelpers.instance.setIsInitialOrientationFlipped(mainViewModel.activity.display?.rotation == 3)
_guestThread = thread(start = true) { _guestThread = thread(start = true) {
runGame() runGame()
} }
@ -167,6 +163,10 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
mainViewModel.performanceManager?.closeCurrentRenderingSession() mainViewModel.performanceManager?.closeCurrentRenderingSession()
} }
} }
thread {
mainViewModel.activity.uiHandler.listen()
}
_nativeRyujinx.graphicsRendererRunLoop() _nativeRyujinx.graphicsRendererRunLoop()
game?.close() game?.close()

View File

@ -72,6 +72,7 @@ class Helpers {
} }
return null return null
} }
fun copyToData( fun copyToData(
file: DocumentFile, path: String, storageHelper: SimpleStorageHelper, file: DocumentFile, path: String, storageHelper: SimpleStorageHelper,
isCopying: MutableState<Boolean>, isCopying: MutableState<Boolean>,
@ -79,17 +80,18 @@ class Helpers {
currentProgressName: MutableState<String>, currentProgressName: MutableState<String>,
finish: () -> Unit finish: () -> Unit
) { ) {
var fPath = path + "/${file.name}";
var callback: FileCallback? = object : FileCallback() { var callback: FileCallback? = object : FileCallback() {
override fun onFailed(errorCode: FileCallback.ErrorCode) { override fun onFailed(errorCode: FileCallback.ErrorCode) {
super.onFailed(errorCode) super.onFailed(errorCode)
File(path).delete() File(fPath).delete()
finish() finish()
} }
override fun onStart(file: Any, workerThread: Thread): Long { override fun onStart(file: Any, workerThread: Thread): Long {
copyProgress.value = 0f copyProgress.value = 0f
(file as DocumentFile)?.apply { (file as DocumentFile).apply {
currentProgressName.value = "Copying ${file.name}" currentProgressName.value = "Copying ${file.name}"
} }
return super.onStart(file, workerThread) return super.onStart(file, workerThread)
@ -98,7 +100,7 @@ class Helpers {
override fun onReport(report: Report) { override fun onReport(report: Report) {
super.onReport(report) super.onReport(report)
if(!isCopying.value) { if (!isCopying.value) {
Thread.currentThread().interrupt() Thread.currentThread().interrupt()
} }
@ -113,17 +115,16 @@ class Helpers {
} }
val ioScope = CoroutineScope(Dispatchers.IO) val ioScope = CoroutineScope(Dispatchers.IO)
isCopying.value = true isCopying.value = true
File(fPath).delete()
file.apply { file.apply {
if (!File(path + "/${file.name}").exists()) { val f = this
val f = this ioScope.launch {
ioScope.launch { f.copyFileTo(
f.copyFileTo( storageHelper.storage.context,
storageHelper.storage.context, File(path),
File(path), callback = callback!!
callback = callback!! )
)
}
} }
} }
} }

View File

@ -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
}

View File

@ -18,8 +18,10 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import com.anggrayudi.storage.SimpleStorageHelper import com.anggrayudi.storage.SimpleStorageHelper
import com.halilibo.richtext.ui.RichTextThemeIntegration
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
import org.ryujinx.android.viewmodels.MainViewModel import org.ryujinx.android.viewmodels.MainViewModel
import org.ryujinx.android.viewmodels.QuickSettings
import org.ryujinx.android.views.MainView import org.ryujinx.android.views.MainView
import kotlin.math.abs import kotlin.math.abs
@ -27,9 +29,11 @@ import kotlin.math.abs
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
private var physicalControllerManager: PhysicalControllerManager = private var physicalControllerManager: PhysicalControllerManager =
PhysicalControllerManager(this) PhysicalControllerManager(this)
private lateinit var motionSensorManager: MotionSensorManager
private var _isInit: Boolean = false private var _isInit: Boolean = false
var isGameRunning = false var isGameRunning = false
var storageHelper: SimpleStorageHelper? = null var storageHelper: SimpleStorageHelper? = null
lateinit var uiHandler: UiHandler
companion object { companion object {
var mainViewModel: MainViewModel? = null var mainViewModel: MainViewModel? = null
var AppPath : String = "" var AppPath : String = ""
@ -64,12 +68,27 @@ class MainActivity : BaseActivity() {
return return
val appPath: String = AppPath 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 _isInit = success
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
motionSensorManager = MotionSensorManager(this)
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
if( if(
!Environment.isExternalStorageManager() !Environment.isExternalStorageManager()
) { ) {
@ -78,7 +97,6 @@ class MainActivity : BaseActivity() {
AppPath = this.getExternalFilesDir(null)!!.absolutePath AppPath = this.getExternalFilesDir(null)!!.absolutePath
initialize() initialize()
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
@ -86,16 +104,21 @@ class MainActivity : BaseActivity() {
mainViewModel = MainViewModel(this) mainViewModel = MainViewModel(this)
mainViewModel!!.physicalControllerManager = physicalControllerManager mainViewModel!!.physicalControllerManager = physicalControllerManager
mainViewModel!!.motionSensorManager = motionSensorManager
mainViewModel!!.refreshFirmwareVersion()
mainViewModel?.apply { mainViewModel?.apply {
setContent { setContent {
RyujinxAndroidTheme { RyujinxAndroidTheme {
// A surface container using the 'background' color from the theme RichTextThemeIntegration(contentColor = { MaterialTheme.colorScheme.onSurface }) {
Surface( // A surface container using the 'background' color from the theme
modifier = Modifier.fillMaxSize(), Surface(
color = MaterialTheme.colorScheme.background modifier = Modifier.fillMaxSize(),
) { color = MaterialTheme.colorScheme.background
MainView.Main(mainViewModel = this) ) {
MainView.Main(mainViewModel = this)
}
} }
} }
} }
@ -133,7 +156,7 @@ class MainActivity : BaseActivity() {
fun setFullScreen(fullscreen: Boolean) { fun setFullScreen(fullscreen: Boolean) {
requestedOrientation = 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) val insets = WindowCompat.getInsetsController(window, window.decorView)
@ -183,6 +206,8 @@ class MainActivity : BaseActivity() {
setFullScreen(true) setFullScreen(true)
NativeHelpers.instance.setTurboMode(true) NativeHelpers.instance.setTurboMode(true)
force60HzRefreshRate(true) force60HzRefreshRate(true)
if (QuickSettings(this).enableMotion)
motionSensorManager.register()
} }
} }
@ -193,5 +218,7 @@ class MainActivity : BaseActivity() {
NativeHelpers.instance.setTurboMode(false) NativeHelpers.instance.setTurboMode(false)
force60HzRefreshRate(false) force60HzRefreshRate(false)
} }
motionSensorManager.unregister()
} }
} }

View File

@ -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?) {
}
}

View File

@ -31,4 +31,14 @@ class NativeHelpers {
external fun getProgressValue() : Float external fun getProgressValue() : Float
external fun storeStringJava(string: String) : Long external fun storeStringJava(string: String) : Long
external fun getStringJava(id: Long) : String 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
} }

View File

@ -1,17 +1,19 @@
package org.ryujinx.android package org.ryujinx.android
import android.view.InputDevice
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import org.ryujinx.android.viewmodels.QuickSettings
class PhysicalControllerManager(val activity: MainActivity) { class PhysicalControllerManager(val activity: MainActivity) {
private var controllerId: Int = -1 private var controllerId: Int = -1
private var ryujinxNative: RyujinxNative = RyujinxNative.instance private var ryujinxNative: RyujinxNative = RyujinxNative.instance
fun onKeyEvent(event: KeyEvent) : Boolean{ fun onKeyEvent(event: KeyEvent) : Boolean{
if(controllerId != -1) { val id = getGamePadButtonInputId(event.keyCode)
val id = getGamePadButtonInputId(event.keyCode) if(id != GamePadButtonInputId.None) {
val isNotFallback = (event.flags and KeyEvent.FLAG_FALLBACK) == 0
if(id != GamePadButtonInputId.None) { if (/*controllerId != -1 &&*/ isNotFallback) {
when (event.action) { when (event.action) {
KeyEvent.ACTION_UP -> { KeyEvent.ACTION_UP -> {
ryujinxNative.inputSetButtonReleased(id.ordinal, controllerId) ryujinxNative.inputSetButtonReleased(id.ordinal, controllerId)
@ -23,13 +25,16 @@ class PhysicalControllerManager(val activity: MainActivity) {
} }
return true return true
} }
else if(!isNotFallback){
return true
}
} }
return false return false
} }
fun onMotionEvent(ev: MotionEvent) { fun onMotionEvent(ev: MotionEvent) {
if(controllerId != -1) { if(true) {
if(ev.action == MotionEvent.ACTION_MOVE) { if(ev.action == MotionEvent.ACTION_MOVE) {
val leftStickX = ev.getAxisValue(MotionEvent.AXIS_X) val leftStickX = ev.getAxisValue(MotionEvent.AXIS_X)
val leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y) val leftStickY = ev.getAxisValue(MotionEvent.AXIS_Y)
@ -37,20 +42,60 @@ class PhysicalControllerManager(val activity: MainActivity) {
val rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ) val rightStickY = ev.getAxisValue(MotionEvent.AXIS_RZ)
ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId) ryujinxNative.inputSetStickAxis(1, leftStickX, -leftStickY ,controllerId)
ryujinxNative.inputSetStickAxis(2, rightStickX, -rightStickY ,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) controllerId = ryujinxNative.inputConnectGamepad(0)
return controllerId
}
fun disconnect(){
controllerId = -1
} }
private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId { private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
val quickSettings = QuickSettings(activity)
return when (keycode) { return when (keycode) {
KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B KeyEvent.KEYCODE_BUTTON_A -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.A else GamePadButtonInputId.B
KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A KeyEvent.KEYCODE_BUTTON_B -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.B else GamePadButtonInputId.A
KeyEvent.KEYCODE_BUTTON_X -> GamePadButtonInputId.X KeyEvent.KEYCODE_BUTTON_X -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.X else GamePadButtonInputId.Y
KeyEvent.KEYCODE_BUTTON_Y -> GamePadButtonInputId.Y KeyEvent.KEYCODE_BUTTON_Y -> if (!quickSettings.useSwitchLayout) GamePadButtonInputId.Y else GamePadButtonInputId.X
KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder KeyEvent.KEYCODE_BUTTON_L1 -> GamePadButtonInputId.LeftShoulder
KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger KeyEvent.KEYCODE_BUTTON_L2 -> GamePadButtonInputId.LeftTrigger
KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder KeyEvent.KEYCODE_BUTTON_R1 -> GamePadButtonInputId.RightShoulder

View File

@ -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
}
}

View File

@ -4,24 +4,28 @@ import org.ryujinx.android.viewmodels.GameInfo
@Suppress("KotlinJniMissingFunction") @Suppress("KotlinJniMissingFunction")
class RyujinxNative { class RyujinxNative {
external fun initialize(appPath: Long, enableDebugLogs : Boolean): Boolean external fun initialize(appPath: Long): Boolean
companion object { companion object {
val instance: RyujinxNative = RyujinxNative() val instance: RyujinxNative = RyujinxNative()
init { init {
System.loadLibrary("ryujinx") System.loadLibrary("ryujinx")
} }
} }
external fun deviceInitialize(isHostMapped: Boolean, useNce: Boolean, external fun deviceInitialize(
systemLanguage : Int, isHostMapped: Boolean, useNce: Boolean,
regionCode : Int, systemLanguage: Int,
enableVsync : Boolean, regionCode: Int,
enableDockedMode : Boolean, enableVsync: Boolean,
enablePtc : Boolean, enableDockedMode: Boolean,
enableInternetAccess : Boolean, enablePtc: Boolean,
timeZone : Long, enableInternetAccess: Boolean,
ignoreMissingServices : Boolean): Boolean timeZone: Long,
ignoreMissingServices: Boolean
): Boolean
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
external fun graphicsInitializeRenderer( external fun graphicsInitializeRenderer(
extensions: Array<String>, extensions: Array<String>,
@ -33,9 +37,9 @@ class RyujinxNative {
external fun deviceGetGameFrameRate(): Double external fun deviceGetGameFrameRate(): Double
external fun deviceGetGameFrameTime(): Double external fun deviceGetGameFrameTime(): Double
external fun deviceGetGameFifo(): 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 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 graphicsRendererSetSize(width: Int, height: Int)
external fun graphicsRendererSetVsync(enabled: Boolean) external fun graphicsRendererSetVsync(enabled: Boolean)
external fun graphicsRendererRunLoop() external fun graphicsRendererRunLoop()
@ -49,19 +53,29 @@ class RyujinxNative {
external fun inputSetButtonReleased(button: Int, id: Int) external fun inputSetButtonReleased(button: Int, id: Int)
external fun inputConnectGamepad(index: Int): Int external fun inputConnectGamepad(index: Int): Int
external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: 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 graphicsSetSurface(surface: Long, window: Long)
external fun deviceCloseEmulation() external fun deviceCloseEmulation()
external fun deviceSignalEmulationClose() external fun deviceSignalEmulationClose()
external fun deviceGetDlcTitleId(path: Long, ncaPath: Long) : Long external fun deviceGetDlcTitleId(path: Long, ncaPath: Long): Long
external fun deviceGetDlcContentList(path: Long, titleId: Long) : Array<String> external fun deviceGetDlcContentList(path: Long, titleId: Long): Array<String>
external fun userGetOpenedUser() : Long external fun userGetOpenedUser(): Long
external fun userGetUserPicture(userId: Long) : Long external fun userGetUserPicture(userId: Long): Long
external fun userSetUserPicture(userId: String, picture: String) 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 userSetUserName(userId: String, userName: String)
external fun userGetAllUsers() : Array<String> external fun userGetAllUsers(): Array<String>
external fun userAddUser(username: String, picture: String) external fun userAddUser(username: String, picture: String)
external fun userDeleteUser(userId: String) external fun userDeleteUser(userId: String)
external fun userOpenUser(userId: Long) external fun userOpenUser(userId: Long)
external fun userCloseUser(userId: String) 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)
} }

View File

@ -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")
}
}
}
}
}
}
}
}
}

View File

@ -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<String> = 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<String> = 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<out String>?) : 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<out String>?) : 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<out String>?, 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)
}
}

View File

@ -40,7 +40,7 @@ class DlcViewModel(val titleId: String) {
val path = file.getAbsolutePath(storageHelper.storage.context) val path = file.getAbsolutePath(storageHelper.storage.context)
if (path.isNotEmpty()) { if (path.isNotEmpty()) {
data?.apply { data?.apply {
var contents = RyujinxNative.instance.deviceGetDlcContentList( val contents = RyujinxNative.instance.deviceGetDlcContentList(
NativeHelpers.instance.storeStringJava(path), NativeHelpers.instance.storeStringJava(path),
titleId.toLong(16) titleId.toLong(16)
) )

View File

@ -1,13 +1,17 @@
package org.ryujinx.android.viewmodels package org.ryujinx.android.viewmodels
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.extension import com.anggrayudi.storage.file.extension
import org.ryujinx.android.NativeHelpers
import org.ryujinx.android.RyujinxNative import org.ryujinx.android.RyujinxNative
class GameModel(var file: DocumentFile, val context: Context) { class GameModel(var file: DocumentFile, val context: Context) {
private var updateDescriptor: ParcelFileDescriptor? = null
var type: FileType
var descriptor: ParcelFileDescriptor? = null var descriptor: ParcelFileDescriptor? = null
var fileName: String? var fileName: String?
var fileSize = 0.0 var fileSize = 0.0
@ -19,8 +23,9 @@ class GameModel(var file: DocumentFile, val context: Context) {
init { init {
fileName = file.name fileName = file.name
var pid = open() val pid = open()
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, file.extension.contains("xci")) val ext = NativeHelpers.instance.storeStringJava(file.extension)
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, ext)
close() close()
fileSize = gameInfo.FileSize fileSize = gameInfo.FileSize
@ -29,6 +34,16 @@ class GameModel(var file: DocumentFile, val context: Context) {
developer = gameInfo.Developer developer = gameInfo.Developer
version = gameInfo.Version version = gameInfo.Version
icon = gameInfo.Icon 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 { fun open() : Int {
@ -37,13 +52,29 @@ class GameModel(var file: DocumentFile, val context: Context) {
return descriptor?.fd ?: 0 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() { fun close() {
descriptor?.close() descriptor?.close()
descriptor = null descriptor = null
} updateDescriptor?.close()
updateDescriptor = null
fun isXci() : Boolean {
return file.extension == "xci"
} }
} }
@ -55,3 +86,10 @@ class GameInfo {
var Version: String? = null var Version: String? = null
var Icon: String? = null var Icon: String? = null
} }
enum class FileType{
None,
Nsp,
Xci,
Nro
}

View File

@ -6,19 +6,20 @@ import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.anggrayudi.storage.file.DocumentFileCompat import com.anggrayudi.storage.file.DocumentFileCompat
import com.anggrayudi.storage.file.DocumentFileType import com.anggrayudi.storage.file.DocumentFileType
import com.anggrayudi.storage.file.FileFullPath
import com.anggrayudi.storage.file.extension import com.anggrayudi.storage.file.extension
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.search import com.anggrayudi.storage.file.search
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import java.util.Locale
import kotlin.concurrent.thread import kotlin.concurrent.thread
class HomeViewModel( class HomeViewModel(
val activity: MainActivity? = null, val activity: MainActivity? = null,
val mainViewModel: MainViewModel? = null val mainViewModel: MainViewModel? = null
) { ) {
private var shouldReload: Boolean = false
private var savedFolder: String = ""
private var isLoading: Boolean = false private var isLoading: Boolean = false
private var loadedCache: List<GameModel> = listOf() private var loadedCache: MutableList<GameModel> = mutableListOf()
private var gameFolderPath: DocumentFile? = null private var gameFolderPath: DocumentFile? = null
private var sharedPref: SharedPreferences? = null private var sharedPref: SharedPreferences? = null
val gameList: SnapshotStateList<GameModel> = SnapshotStateList() val gameList: SnapshotStateList<GameModel> = SnapshotStateList()
@ -26,46 +27,34 @@ class HomeViewModel(
init { init {
if (activity != null) { if (activity != null) {
sharedPref = PreferenceManager.getDefaultSharedPreferences(activity) 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() { fun ensureReloadIfNecessary() {
val path = sharedPref?.getString("gameFolder", "") ?: "" val oldFolder = savedFolder
savedFolder = sharedPref?.getString("gameFolder", "") ?: ""
if (path.isEmpty()) if (savedFolder.isNotEmpty() && (shouldReload || savedFolder != oldFolder)) {
activity?.storageHelper?.storage?.openFolderPicker() gameFolderPath = DocumentFileCompat.fromFullPath(
else mainViewModel?.activity!!,
activity?.storageHelper?.storage?.openFolderPicker( savedFolder,
activity.storageHelper!!.storage.requestCodeFolderPicker, documentType = DocumentFileType.FOLDER,
FileFullPath(activity, path) 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() { fun reloadGameList() {
@ -80,26 +69,24 @@ class HomeViewModel(
isLoading = true isLoading = true
thread { thread {
try { try {
loadedCache.clear()
val files = mutableListOf<GameModel>() val files = mutableListOf<GameModel>()
for (file in folder.search(false, DocumentFileType.FILE)) { 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 { activity.let {
val item = GameModel(file, it) 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 isLoading = false
} finally { } finally {
isLoading = false isLoading = false
} }
} }
} }
fun clearLoadedCache(){
loadedCache = listOf()
}
} }

View File

@ -12,7 +12,9 @@ import kotlinx.coroutines.sync.Semaphore
import org.ryujinx.android.GameController import org.ryujinx.android.GameController
import org.ryujinx.android.GameHost import org.ryujinx.android.GameHost
import org.ryujinx.android.GraphicsConfiguration import org.ryujinx.android.GraphicsConfiguration
import org.ryujinx.android.Logging
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import org.ryujinx.android.MotionSensorManager
import org.ryujinx.android.NativeGraphicsInterop import org.ryujinx.android.NativeGraphicsInterop
import org.ryujinx.android.NativeHelpers import org.ryujinx.android.NativeHelpers
import org.ryujinx.android.PerformanceManager import org.ryujinx.android.PerformanceManager
@ -25,12 +27,15 @@ import java.io.File
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
class MainViewModel(val activity: MainActivity) { class MainViewModel(val activity: MainActivity) {
var physicalControllerManager: PhysicalControllerManager? = null var physicalControllerManager: PhysicalControllerManager? = null
var motionSensorManager: MotionSensorManager? = null
var gameModel: GameModel? = null var gameModel: GameModel? = null
var controller: GameController? = null var controller: GameController? = null
var performanceManager: PerformanceManager? = null var performanceManager: PerformanceManager? = null
var selected: GameModel? = null var selected: GameModel? = null
var isMiiEditorLaunched = false var isMiiEditorLaunched = false
val userViewModel = UserViewModel() val userViewModel = UserViewModel()
val logging = Logging(this)
var firmwareVersion = ""
private var gameTimeState: MutableState<Double>? = null private var gameTimeState: MutableState<Double>? = null
private var gameFpsState: MutableState<Double>? = null private var gameFpsState: MutableState<Double>? = null
private var fifoState: MutableState<Double>? = null private var fifoState: MutableState<Double>? = null
@ -38,6 +43,7 @@ class MainViewModel(val activity: MainActivity) {
private var progressValue: MutableState<Float>? = null private var progressValue: MutableState<Float>? = null
private var showLoading: MutableState<Boolean>? = null private var showLoading: MutableState<Boolean>? = null
private var refreshUser: MutableState<Boolean>? = null private var refreshUser: MutableState<Boolean>? = null
var gameHost: GameHost? = null var gameHost: GameHost? = null
set(value) { set(value) {
field = value field = value
@ -59,6 +65,16 @@ class MainViewModel(val activity: MainActivity) {
RyujinxNative.instance.deviceSignalEmulationClose() RyujinxNative.instance.deviceSignalEmulationClose()
gameHost?.close() gameHost?.close()
RyujinxNative.instance.deviceCloseEmulation() 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 { fun loadGame(game:GameModel) : Boolean {
@ -69,6 +85,8 @@ class MainViewModel(val activity: MainActivity) {
if (descriptor == 0) if (descriptor == 0)
return false return false
val update = game.openUpdate()
gameModel = game gameModel = game
isMiiEditorLaunched = false isMiiEditorLaunched = false
@ -162,7 +180,7 @@ class MainViewModel(val activity: MainActivity) {
if (!success) if (!success)
return false return false
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.isXci()) success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.type.ordinal, update)
if (!success) if (!success)
return false return false
@ -170,8 +188,6 @@ class MainViewModel(val activity: MainActivity) {
return true return true
} }
fun loadMiiEditor() : Boolean { fun loadMiiEditor() : Boolean {
val nativeRyujinx = RyujinxNative.instance val nativeRyujinx = RyujinxNative.instance
@ -351,6 +367,8 @@ class MainViewModel(val activity: MainActivity) {
activity.setFullScreen(true) activity.setFullScreen(true)
navController?.navigate("game") navController?.navigate("game")
activity.isGameRunning = true activity.isGameRunning = true
if (QuickSettings(activity).enableMotion)
motionSensorManager?.register()
} }
fun setProgressStates( fun setProgressStates(

View File

@ -15,6 +15,19 @@ class QuickSettings(val activity: Activity) {
var enableShaderCache: Boolean var enableShaderCache: Boolean
var enableTextureRecompression: Boolean var enableTextureRecompression: Boolean
var resScale : Float 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) private var sharedPref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
@ -29,5 +42,46 @@ class QuickSettings(val activity: Activity) {
enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false) enableTextureRecompression = sharedPref.getBoolean("enableTextureRecompression", false)
resScale = sharedPref.getFloat("resScale", 1f) resScale = sharedPref.getFloat("resScale", 1f)
useVirtualController = sharedPref.getBoolean("useVirtualController", true) 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()
} }
} }

View File

@ -2,18 +2,43 @@ package org.ryujinx.android.viewmodels
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.documentfile.provider.DocumentFile
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.preference.PreferenceManager 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.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) { class SettingsViewModel(var navController: NavHostController, val activity: MainActivity) {
var selectedFirmwareVersion: String = ""
private var previousFileCallback: ((requestCode: Int, files: List<DocumentFile>) -> Unit)?
private var previousFolderCallback: ((requestCode: Int, folder: DocumentFile) -> Unit)?
private var sharedPref: SharedPreferences private var sharedPref: SharedPreferences
var selectedFirmwareFile: DocumentFile? = null
init { init {
sharedPref = getPreferences() 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) return PreferenceManager.getDefaultSharedPreferences(activity)
} }
@ -27,9 +52,19 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enableShaderCache: MutableState<Boolean>, enableShaderCache: MutableState<Boolean>,
enableTextureRecompression: MutableState<Boolean>, enableTextureRecompression: MutableState<Boolean>,
resScale: MutableState<Float>, resScale: MutableState<Float>,
useVirtualController: MutableState<Boolean> useVirtualController: MutableState<Boolean>,
) isGrid: MutableState<Boolean>,
{ useSwitchLayout: MutableState<Boolean>,
enableMotion: MutableState<Boolean>,
enableDebugLogs: MutableState<Boolean>,
enableStubLogs: MutableState<Boolean>,
enableInfoLogs: MutableState<Boolean>,
enableWarningLogs: MutableState<Boolean>,
enableErrorLogs: MutableState<Boolean>,
enableGuestLogs: MutableState<Boolean>,
enableAccessLogs: MutableState<Boolean>,
enableTraceLogs: MutableState<Boolean>
) {
isHostMapped.value = sharedPref.getBoolean("isHostMapped", true) isHostMapped.value = sharedPref.getBoolean("isHostMapped", true)
useNce.value = sharedPref.getBoolean("useNce", 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) enablePtc.value = sharedPref.getBoolean("enablePtc", true)
ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false) ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false)
enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true) enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false) enableTextureRecompression.value =
sharedPref.getBoolean("enableTextureRecompression", false)
resScale.value = sharedPref.getFloat("resScale", 1f) resScale.value = sharedPref.getFloat("resScale", 1f)
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true) 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( fun save(
@ -53,8 +101,19 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
enableShaderCache: MutableState<Boolean>, enableShaderCache: MutableState<Boolean>,
enableTextureRecompression: MutableState<Boolean>, enableTextureRecompression: MutableState<Boolean>,
resScale: MutableState<Float>, resScale: MutableState<Float>,
useVirtualController: MutableState<Boolean> useVirtualController: MutableState<Boolean>,
){ isGrid: MutableState<Boolean>,
useSwitchLayout: MutableState<Boolean>,
enableMotion: MutableState<Boolean>,
enableDebugLogs: MutableState<Boolean>,
enableStubLogs: MutableState<Boolean>,
enableInfoLogs: MutableState<Boolean>,
enableWarningLogs: MutableState<Boolean>,
enableErrorLogs: MutableState<Boolean>,
enableGuestLogs: MutableState<Boolean>,
enableAccessLogs: MutableState<Boolean>,
enableTraceLogs: MutableState<Boolean>
) {
val editor = sharedPref.edit() val editor = sharedPref.edit()
editor.putBoolean("isHostMapped", isHostMapped.value) editor.putBoolean("isHostMapped", isHostMapped.value)
@ -67,7 +126,148 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value) editor.putBoolean("enableTextureRecompression", enableTextureRecompression.value)
editor.putFloat("resScale", resScale.value) editor.putFloat("resScale", resScale.value)
editor.putBoolean("useVirtualController", useVirtualController.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() 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)
} }
}
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<FirmwareInstallState>) {
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<FirmwareInstallState>) {
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<FirmwareInstallState>){
selectedFirmwareFile = null
selectedFirmwareVersion = ""
installState.value = FirmwareInstallState.None
}
}
enum class FirmwareInstallState{
None,
Cancelled,
Verifying,
Query,
Install,
Done
}

View File

@ -1,12 +1,15 @@
package org.ryujinx.android.viewmodels package org.ryujinx.android.viewmodels
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.text.toLowerCase
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.SimpleStorageHelper import com.anggrayudi.storage.SimpleStorageHelper
import com.anggrayudi.storage.file.extension
import com.google.gson.Gson import com.google.gson.Gson
import org.ryujinx.android.Helpers
import org.ryujinx.android.MainActivity import org.ryujinx.android.MainActivity
import java.io.File import java.io.File
import kotlin.math.max import kotlin.math.max
@ -16,29 +19,32 @@ class TitleUpdateViewModel(val titleId: String) {
private var basePath: String private var basePath: String
private var updateJsonName = "updates.json" private var updateJsonName = "updates.json"
private var storageHelper: SimpleStorageHelper private var storageHelper: SimpleStorageHelper
var currentPaths: MutableList<String> = mutableListOf()
var pathsState: SnapshotStateList<String>? = null var pathsState: SnapshotStateList<String>? = null
companion object { companion object {
const val UpdateRequestCode = 1002 const val UpdateRequestCode = 1002
} }
fun Remove(index: Int) { fun remove(index: Int) {
if (index <= 0) if (index <= 0)
return return
data?.paths?.apply { data?.paths?.apply {
val removed = removeAt(index - 1) val str = removeAt(index - 1)
File(removed).deleteRecursively() Uri.parse(str)?.apply {
storageHelper.storage.context.contentResolver.releasePersistableUriPermission(
this,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
pathsState?.clear() pathsState?.clear()
pathsState?.addAll(this) pathsState?.addAll(this)
currentPaths = this
} }
} }
fun Add( fun add() {
isCopying: MutableState<Boolean>,
copyProgress: MutableState<Float>,
currentProgressName: MutableState<String>
) {
val callBack = storageHelper.onFileSelected val callBack = storageHelper.onFileSelected
storageHelper.onFileSelected = { requestCode, files -> storageHelper.onFileSelected = { requestCode, files ->
@ -47,29 +53,29 @@ class TitleUpdateViewModel(val titleId: String) {
if (requestCode == UpdateRequestCode) { if (requestCode == UpdateRequestCode) {
val file = files.firstOrNull() val file = files.firstOrNull()
file?.apply { file?.apply {
// Copy updates to internal data folder if(file.extension == "nsp"){
val updatePath = "$basePath/update" storageHelper.storage.context.contentResolver.takePersistableUriPermission(file.uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
File(updatePath).mkdirs() currentPaths.add(file.uri.toString())
Helpers.copyToData( }
this,
updatePath,
storageHelper,
isCopying,
copyProgress,
currentProgressName, ::refreshPaths
)
} }
refreshPaths()
} }
} }
} }
storageHelper.openFilePicker(UpdateRequestCode) storageHelper.openFilePicker(UpdateRequestCode)
} }
fun refreshPaths() { private fun refreshPaths() {
data?.apply { data?.apply {
val updatePath = "$basePath/update"
val existingPaths = mutableListOf<String>() val existingPaths = mutableListOf<String>()
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)) { if (!existingPaths.contains(selected)) {
selected = "" selected = ""
@ -88,7 +94,6 @@ class TitleUpdateViewModel(val titleId: String) {
openDialog: MutableState<Boolean> openDialog: MutableState<Boolean>
) { ) {
data?.apply { data?.apply {
val updatePath = "$basePath/update"
this.selected = "" this.selected = ""
if (paths.isNotEmpty() && index > 0) { if (paths.isNotEmpty() && index > 0) {
val ind = max(index - 1, paths.count() - 1) val ind = max(index - 1, paths.count() - 1)
@ -98,18 +103,29 @@ class TitleUpdateViewModel(val titleId: String) {
File(basePath).mkdirs() File(basePath).mkdirs()
var metadata = TitleUpdateMetadata() val metadata = TitleUpdateMetadata()
val savedUpdates = mutableListOf<String>() val savedUpdates = mutableListOf<String>()
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 metadata.paths = savedUpdates
val selectedName = File(selected).name if(selected.isNotEmpty()){
val newSelectedPath = "$updatePath/$selectedName" val uri = Uri.parse(selected)
if (File(newSelectedPath).exists()) { val file = DocumentFile.fromSingleUri(storageHelper.storage.context, uri)
metadata.selected = newSelectedPath 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) File("$basePath/$updateJsonName").writeText(json)
openDialog.value = false openDialog.value = false
@ -137,10 +153,13 @@ class TitleUpdateViewModel(val titleId: String) {
val gson = Gson() val gson = Gson()
data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java) data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java)
refreshPaths()
} }
currentPaths = data?.paths ?: mutableListOf()
storageHelper = MainActivity.StorageHelper!! storageHelper = MainActivity.StorageHelper!!
refreshPaths()
File("$basePath/update").deleteRecursively()
} }
} }

View File

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -51,25 +50,13 @@ class DlcViews {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Column { Column {
Row(modifier = Modifier.padding(8.dp) Row(modifier = Modifier
.padding(8.dp)
.fillMaxWidth(), .fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween) { horizontalArrangement = Arrangement.SpaceBetween) {
Text(text = "DLC for ${name}", textAlign = TextAlign.Center, modifier = Modifier.align( Text(text = "DLC for ${name}", textAlign = TextAlign.Center, modifier = Modifier.align(
Alignment.CenterVertically Alignment.CenterVertically
)) ))
IconButton(
onClick = {
viewModel.add(refresh)
},
modifier = Modifier.align(
Alignment.CenterVertically
)
) {
Icon(
Icons.Filled.Add,
contentDescription = "Add"
)
}
} }
Surface( Surface(
modifier = Modifier modifier = Modifier
@ -119,16 +106,27 @@ class DlcViews {
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
TextButton( Row(modifier = Modifier.align(Alignment.End)) {
modifier = Modifier.align(Alignment.End), TextButton(
onClick = { modifier = Modifier.padding(4.dp),
openDialog.value = false onClick = {
viewModel.save(dlcList) viewModel.add(refresh)
}, }
) { ) {
Text("Save")
Text("Add")
}
TextButton(
modifier = Modifier.padding(4.dp),
onClick = {
openDialog.value = false
viewModel.save(dlcList)
},
) {
Text("Save")
}
} }
} }
} }
} }
} }

View File

@ -20,6 +20,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -82,6 +83,9 @@ class GameViews {
val enableVsync = remember { val enableVsync = remember {
mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync) mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync)
} }
val enableMotion = remember {
mutableStateOf(QuickSettings(mainViewModel.activity).enableMotion)
}
val showMore = remember { val showMore = remember {
mutableStateOf(false) mutableStateOf(false)
} }
@ -169,27 +173,56 @@ class GameViews {
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
shape = MaterialTheme.shapes.medium shape = MaterialTheme.shapes.medium
) { ) {
Row(modifier = Modifier.padding(8.dp)) { Column {
IconButton(modifier = Modifier.padding(4.dp), onClick = { Row(
showMore.value = false modifier = Modifier,
showController.value = !showController.value horizontalArrangement = Arrangement.SpaceBetween,
mainViewModel.controller?.setVisible(showController.value) verticalAlignment = Alignment.CenterVertically
}) { ) {
Icon( Text(
imageVector = Icons.videoGame(), text = "Enable Motion",
contentDescription = "Toggle Virtual Pad" 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 = { Row(modifier = Modifier.padding(8.dp),
showMore.value = false horizontalArrangement = Arrangement.SpaceBetween) {
enableVsync.value = !enableVsync.value IconButton(modifier = Modifier.padding(4.dp), onClick = {
RyujinxNative.instance.graphicsRendererSetVsync(enableVsync.value) showMore.value = false
}) { showController.value = !showController.value
Icon( ryujinxNative.inputReleaseTouchPoint()
imageVector = Icons.vSync(), mainViewModel.controller?.setVisible(showController.value)
tint = if (enableVsync.value) Color.Green else Color.Red, }) {
contentDescription = "Toggle VSync" 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( Surface(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.surface.copy(0.4f) color = MaterialTheme.colorScheme.background.copy(0.4f)
) { ) {
Column { Column {
var gameTimeVal = 0.0 var gameTimeVal = 0.0

View File

@ -4,6 +4,7 @@ import android.content.res.Resources
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn 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.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Menu 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.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SearchBarDefaults
@ -50,16 +52,22 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.anggrayudi.storage.extension.launchOnUiThread 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.GameModel
import org.ryujinx.android.viewmodels.HomeViewModel import org.ryujinx.android.viewmodels.HomeViewModel
import org.ryujinx.android.viewmodels.QuickSettings
import java.util.Base64 import java.util.Base64
import java.util.Locale import java.util.Locale
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -67,7 +75,8 @@ import kotlin.math.roundToInt
class HomeViews { class HomeViews {
companion object { companion object {
const val ImageSize = 150 const val ListImageSize = 150
const val GridImageSize = 300
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -75,11 +84,15 @@ class HomeViews {
viewModel: HomeViewModel = HomeViewModel(), viewModel: HomeViewModel = HomeViewModel(),
navController: NavHostController? = null navController: NavHostController? = null
) { ) {
viewModel.ensureReloadIfNecessary()
val showAppActions = remember { mutableStateOf(false) } val showAppActions = remember { mutableStateOf(false) }
val showLoading = remember { mutableStateOf(false) } val showLoading = remember { mutableStateOf(false) }
val openTitleUpdateDialog = remember { mutableStateOf(false) } val openTitleUpdateDialog = remember { mutableStateOf(false) }
val canClose = remember { mutableStateOf(true) } val canClose = remember { mutableStateOf(true) }
val openDlcDialog = remember { mutableStateOf(false) } val openDlcDialog = remember { mutableStateOf(false) }
val selectedModel = remember {
mutableStateOf(viewModel.mainViewModel?.selected)
}
val query = remember { val query = remember {
mutableStateOf("") 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 -> ) { contentPadding ->
Box(modifier = Modifier.padding(contentPadding)) { Box(modifier = Modifier.padding(contentPadding)) {
val list = remember { val list = remember {
viewModel.gameList viewModel.gameList
} }
val selectedModel = remember { viewModel.filter(query.value)
mutableStateOf(viewModel.mainViewModel?.selected) var settings = QuickSettings(viewModel.activity!!)
}
LazyColumn(Modifier.fillMaxSize()) { if (settings.isGrid) {
items(list) { val size = GridImageSize / Resources.getSystem().displayMetrics.density
it.titleName?.apply { LazyVerticalGrid(
if (this.isNotEmpty() && (query.value.trim() columns = GridCells.Adaptive(minSize = (size + 4).dp),
.isEmpty() || this.lowercase( modifier = Modifier
Locale.getDefault() .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)) GridGameItem(
) it,
GameItem( viewModel,
it, showAppActions,
viewModel, showLoading,
showAppActions, selectedModel
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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun GameItem( fun ListGameItem(
gameModel: GameModel, gameModel: GameModel,
viewModel: HomeViewModel, viewModel: HomeViewModel,
showAppActions: MutableState<Boolean>, showAppActions: MutableState<Boolean>,
@ -400,7 +401,7 @@ class HomeViews {
selected = null selected = null
} }
selectedModel.value = 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 { thread {
showLoading.value = true showLoading.value = true
val success = val success =
@ -429,10 +430,11 @@ class HomeViews {
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Row { Row {
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") { if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) {
if (gameModel.icon?.isNotEmpty() == true) { if (gameModel.icon?.isNotEmpty() == true) {
val pic = decoder.decode(gameModel.icon) val pic = decoder.decode(gameModel.icon)
val size = ImageSize / Resources.getSystem().displayMetrics.density val size =
ListImageSize / Resources.getSystem().displayMetrics.density
Image( Image(
bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size) bitmap = BitmapFactory.decodeByteArray(pic, 0, pic.size)
.asImageBitmap(), .asImageBitmap(),
@ -442,7 +444,9 @@ class HomeViews {
.width(size.roundToInt().dp) .width(size.roundToInt().dp)
.height(size.roundToInt().dp) .height(size.roundToInt().dp)
) )
} else NotAvailableIcon() } else if (gameModel.type == FileType.Nro)
NROIcon()
else NotAvailableIcon()
} else NotAvailableIcon() } else NotAvailableIcon()
Column { Column {
Text(text = gameModel.titleName ?: "") Text(text = gameModel.titleName ?: "")
@ -458,12 +462,107 @@ class HomeViews {
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GridGameItem(
gameModel: GameModel,
viewModel: HomeViewModel,
showAppActions: MutableState<Boolean>,
showLoading: MutableState<Boolean>,
selectedModel: MutableState<GameModel?>
) {
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 @Composable
fun NotAvailableIcon() { fun NotAvailableIcon() {
val size = ImageSize / Resources.getSystem().displayMetrics.density val size = ListImageSize / Resources.getSystem().displayMetrics.density
Icon( Icon(
Icons.Filled.Add, 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 modifier = Modifier
.padding(end = 8.dp) .padding(end = 8.dp)
.width(size.roundToInt().dp) .width(size.roundToInt().dp)

View File

@ -1,6 +1,9 @@
package org.ryujinx.android.views package org.ryujinx.android.views
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.provider.DocumentsContract
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.MutableTransitionState
@ -14,6 +17,8 @@ import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -56,6 +61,8 @@ import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.extension import com.anggrayudi.storage.file.extension
import org.ryujinx.android.Helpers import org.ryujinx.android.Helpers
import org.ryujinx.android.MainActivity 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.MainViewModel
import org.ryujinx.android.viewmodels.SettingsViewModel import org.ryujinx.android.viewmodels.SettingsViewModel
import org.ryujinx.android.viewmodels.VulkanDriverViewModel import org.ryujinx.android.viewmodels.VulkanDriverViewModel
@ -66,7 +73,7 @@ class SettingViews {
const val EXPANSTION_TRANSITION_DURATION = 450 const val EXPANSTION_TRANSITION_DURATION = 450
const val IMPORT_CODE = 12341 const val IMPORT_CODE = 12341
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable @Composable
fun Main(settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel) { fun Main(settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel) {
val loaded = remember { val loaded = remember {
@ -103,6 +110,27 @@ class SettingViews {
val useVirtualController = remember { val useVirtualController = remember {
mutableStateOf(true) 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) { if (!loaded.value) {
settingsViewModel.initializeState( settingsViewModel.initializeState(
@ -112,7 +140,18 @@ class SettingViews {
enableShaderCache, enableShaderCache,
enableTextureRecompression, enableTextureRecompression,
resScale, resScale,
useVirtualController useVirtualController,
isGrid,
useSwitchLayout,
enableMotion,
enableDebugLogs,
enableStubLogs,
enableInfoLogs,
enableWarningLogs,
enableErrorLogs,
enableGuestLogs,
enableAccessLogs,
enableTraceLogs
) )
loaded.value = true loaded.value = true
} }
@ -134,7 +173,18 @@ class SettingViews {
enableShaderCache, enableShaderCache,
enableTextureRecompression, enableTextureRecompression,
resScale, resScale,
useVirtualController useVirtualController,
isGrid,
useSwitchLayout,
enableMotion,
enableDebugLogs,
enableStubLogs,
enableInfoLogs,
enableWarningLogs,
enableErrorLogs,
enableGuestLogs,
enableAccessLogs,
enableTraceLogs
) )
settingsViewModel.navController.popBackStack() settingsViewModel.navController.popBackStack()
}) { }) {
@ -145,6 +195,223 @@ class SettingViews {
Column(modifier = Modifier Column(modifier = Modifier
.padding(contentPadding) .padding(contentPadding)
.verticalScroll(rememberScrollState())) { .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") { ExpandableView(onCardArrowClick = { }, title = "System") {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
Row( Row(
@ -330,15 +597,17 @@ class SettingViews {
showImportCompletion.value = false showImportCompletion.value = false
importFile.value = null importFile.value = null
mainViewModel.userViewModel.refreshUsers() mainViewModel.userViewModel.refreshUsers()
mainViewModel.homeViewModel.clearLoadedCache() mainViewModel.homeViewModel.requestReload()
}) { }) {
Card( Card(
modifier = Modifier, modifier = Modifier,
shape = MaterialTheme.shapes.medium shape = MaterialTheme.shapes.medium
) { ) {
Text(modifier = Modifier Text(
.padding(24.dp), modifier = Modifier
text = "App Data import completed.") .padding(24.dp),
text = "App Data import completed."
)
} }
} }
} }
@ -591,6 +860,173 @@ class SettingViews {
useVirtualController.value = !useVirtualController.value 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, enableShaderCache,
enableTextureRecompression, enableTextureRecompression,
resScale, resScale,
useVirtualController useVirtualController,
isGrid,
useSwitchLayout,
enableMotion,
enableDebugLogs,
enableStubLogs,
enableInfoLogs,
enableWarningLogs,
enableErrorLogs,
enableGuestLogs,
enableAccessLogs,
enableTraceLogs
) )
settingsViewModel.navController.popBackStack() settingsViewModel.navController.popBackStack()
} }

View File

@ -1,5 +1,6 @@
package org.ryujinx.android.views package org.ryujinx.android.views
import android.net.Uri
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.material.icons.filled.Delete
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -28,13 +28,19 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile
import org.ryujinx.android.MainActivity
import org.ryujinx.android.viewmodels.TitleUpdateViewModel import org.ryujinx.android.viewmodels.TitleUpdateViewModel
import java.io.File
class TitleUpdateViews { class TitleUpdateViews {
companion object { companion object {
@Composable @Composable
fun Main(titleId: String, name: String, openDialog: MutableState<Boolean>, canClose: MutableState<Boolean>) { fun Main(
titleId: String,
name: String,
openDialog: MutableState<Boolean>,
canClose: MutableState<Boolean>
) {
val viewModel = TitleUpdateViewModel(titleId) val viewModel = TitleUpdateViewModel(titleId)
val selected = remember { mutableStateOf(0) } val selected = remember { mutableStateOf(0) }
@ -43,15 +49,6 @@ class TitleUpdateViews {
} }
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
val isCopying = remember {
mutableStateOf(false)
}
val copyProgress = remember {
mutableStateOf(0.0f)
}
var currentProgressName = remember {
mutableStateOf("Starting Copy")
}
Column { Column {
Text(text = "Updates for ${name}", textAlign = TextAlign.Center) Text(text = "Updates for ${name}", textAlign = TextAlign.Center)
Surface( Surface(
@ -88,18 +85,24 @@ class TitleUpdateViews {
var index = 1 var index = 1
for (path in paths) { for (path in paths) {
val i = index val i = index
Row(modifier = Modifier.padding(8.dp)) { val uri = Uri.parse(path)
RadioButton( val file = DocumentFile.fromSingleUri(
selected = (selected.value == i), MainActivity.mainViewModel!!.activity,
onClick = { selected.value = i }) uri
Text( )
text = File(path).name, file?.apply {
modifier = Modifier Row(modifier = Modifier.padding(8.dp)) {
.fillMaxWidth() RadioButton(
.align(Alignment.CenterVertically) selected = (selected.value == i),
) onClick = { selected.value = i })
Text(
text = file.name ?: "",
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterVertically)
)
}
} }
index++ index++
} }
} }
@ -107,7 +110,7 @@ class TitleUpdateViews {
Row(modifier = Modifier.align(Alignment.End)) { Row(modifier = Modifier.align(Alignment.End)) {
IconButton( IconButton(
onClick = { onClick = {
viewModel.Remove(selected.value) viewModel.remove(selected.value)
} }
) { ) {
Icon( Icon(
@ -118,7 +121,7 @@ class TitleUpdateViews {
IconButton( IconButton(
onClick = { onClick = {
viewModel.Add(isCopying, copyProgress, currentProgressName) viewModel.add()
} }
) { ) {
Icon( 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)) Spacer(modifier = Modifier.height(18.dp))
TextButton( TextButton(
modifier = Modifier.align(Alignment.End), modifier = Modifier.align(Alignment.End),
onClick = { onClick = {
if (!isCopying.value) { canClose.value = true
canClose.value = true viewModel.save(selected.value, openDialog)
viewModel.save(selected.value, openDialog)
}
}, },
) { ) {
Text("Save") Text("Save")

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="external"
path="/" />
<external-files-path
name="external_files"
path="/" />
<files-path
name="files"
path="/" />
</paths>

View File

@ -24,7 +24,7 @@ android.nonTransitiveRClass=true
# Build configuration # Build configuration
# It needs to be set to either "debug" or "release" and can also be overriden on a per build basis # 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. # by adding -Dorg.ryujinx.config=NAME to the command line.
org.ryujinx.config=debug org.ryujinx.config=release
# Controls stripping of symbols from libryujinx # Controls stripping of symbols from libryujinx
# Setting this property to auto causes symbols to be stripped for release builds, # Setting this property to auto causes symbols to be stripped for release builds,
# but not for debug builds. # but not for debug builds.
@ -33,3 +33,4 @@ org.ryujinx.config=debug
org.ryujinx.symbols.strip=auto org.ryujinx.symbols.strip=auto
# Output path of libryujinx.so # Output path of libryujinx.so
org.ryujinx.publish.path=app/src/main/jniLibs/arm64-v8a 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