Kotlin: Code cleanup

This commit is contained in:
KeatonTheBot
2026-01-23 19:48:00 -06:00
parent e49ef8d60e
commit 6a59302104
25 changed files with 120 additions and 137 deletions
@@ -2,12 +2,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<!-- Android 11+ Package Visibility: was diese App sehen/ansprechen darf --> <!-- Android 11+ Package Visibility: What this app is allowed to see/access -->
<queries> <queries>
<!-- Falls andere Flavors/Starter beteiligt sind, hier sichtbar machen --> <!-- If other flavors/starters are involved, make them visible here -->
<package android:name="org.kenjinx.android"/> <package android:name="org.kenjinx.android"/>
<!-- Allgemein: LAUNCHER-Activities und ACTION_VIEW-Dateien auflösbar machen --> <!-- General: Make LAUNCHER activities and ACTION_VIEW files resolvable -->
<intent> <intent>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@@ -18,7 +18,6 @@ import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event import com.swordfish.radialgamepad.library.event.Event
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
@@ -189,12 +188,12 @@ class GameController2(var activity: Activity) : IGameController {
} }
// --- Local copy of the config generation (name = generateConfig2, to avoid conflicts) --- // --- Local copy of the config generation (name = generateConfig2, to avoid conflicts) ---
private fun generateConfig2(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig { private fun generateConfig2(isLeft: Boolean, useSwitchLayout: Boolean): GamePadConfig {
val distance = 0.3f val distance = 0.3f
val buttonScale = 1f val buttonScale = 1f
if (isLeft) { if (isLeft) {
return RadialGamePadConfig( return GamePadConfig(
/* ringSegments = */ 12, /* ringSegments = */ 12,
/* Primary (Stick) */ /* Primary (Stick) */
// IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3 // IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3
@@ -300,7 +299,7 @@ private fun generateConfig2(isLeft: Boolean, useSwitchLayout: Boolean): RadialGa
) )
) )
} else { } else {
return RadialGamePadConfig( return GamePadConfig(
/* ringSegments = */ 12, /* ringSegments = */ 12,
/* Primary (ABXY) */ /* Primary (ABXY) */
PrimaryDialConfig.PrimaryButtons( PrimaryDialConfig.PrimaryButtons(
@@ -18,7 +18,6 @@ import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event import com.swordfish.radialgamepad.library.event.Event
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
@@ -182,12 +181,12 @@ class GameController3(var activity: Activity) : IGameController {
} }
} }
private fun generateConfig3(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig { private fun generateConfig3(isLeft: Boolean, useSwitchLayout: Boolean): GamePadConfig {
val distance = 0.3f val distance = 0.3f
val buttonScale = 1f val buttonScale = 1f
if (isLeft) { if (isLeft) {
return RadialGamePadConfig( return GamePadConfig(
/* ringSegments = */ 12, /* ringSegments = */ 12,
/* Primary (D-Pad) */ /* Primary (D-Pad) */
PrimaryDialConfig.Cross( PrimaryDialConfig.Cross(
@@ -293,7 +292,7 @@ private fun generateConfig3(isLeft: Boolean, useSwitchLayout: Boolean): RadialGa
) )
) )
} else { } else {
return RadialGamePadConfig( return GamePadConfig(
/* ringSegments = */ 12, /* ringSegments = */ 12,
/* Primary (ABXY) */ /* Primary (ABXY) */
PrimaryDialConfig.PrimaryButtons( PrimaryDialConfig.PrimaryButtons(
@@ -18,7 +18,6 @@ import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event import com.swordfish.radialgamepad.library.event.Event
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
@@ -178,12 +177,12 @@ class GameController4(var activity: Activity) : IGameController {
} }
} }
private fun generateConfig4(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig { private fun generateConfig4(isLeft: Boolean, useSwitchLayout: Boolean): GamePadConfig {
val distance = 0.3f val distance = 0.3f
val buttonScale = 1f val buttonScale = 1f
if (isLeft) { if (isLeft) {
return RadialGamePadConfig( return GamePadConfig(
/* ringSegments = */ 12, /* ringSegments = */ 12,
/* Primary (Stick) */ /* Primary (Stick) */
// IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3 // IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3
@@ -271,7 +270,7 @@ private fun generateConfig4(isLeft: Boolean, useSwitchLayout: Boolean): RadialGa
) )
) )
} else { } else {
return RadialGamePadConfig( return GamePadConfig(
/* ringSegments = */ 12, /* ringSegments = */ 12,
/* Primary (ABXY) */ /* Primary (ABXY) */
PrimaryDialConfig.PrimaryButtons( PrimaryDialConfig.PrimaryButtons(
@@ -18,7 +18,6 @@ import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event import com.swordfish.radialgamepad.library.event.Event
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
@@ -178,12 +177,12 @@ class GameController5(var activity: Activity) : IGameController {
} }
} }
private fun generateConfig5(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig { private fun generateConfig5(isLeft: Boolean, useSwitchLayout: Boolean): GamePadConfig {
val distance = 0.3f val distance = 0.3f
val buttonScale = 1f val buttonScale = 1f
if (isLeft) { if (isLeft) {
return RadialGamePadConfig( return GamePadConfig(
/* ringSegments = */ 12, /* ringSegments = */ 12,
/* Primary (Stick) */ /* Primary (Stick) */
// IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3 // IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3
@@ -273,7 +272,7 @@ private fun generateConfig5(isLeft: Boolean, useSwitchLayout: Boolean): RadialGa
) )
) )
} else { } else {
return RadialGamePadConfig( return GamePadConfig(
/* ringSegments = */ 12, /* ringSegments = */ 12,
/* Primary (ABXY) */ /* Primary (ABXY) */
PrimaryDialConfig.PrimaryButtons( PrimaryDialConfig.PrimaryButtons(
@@ -18,7 +18,6 @@ import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event import com.swordfish.radialgamepad.library.event.Event
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
@@ -177,7 +177,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
override fun onWindowVisibilityChanged(visibility: Int) { override fun onWindowVisibilityChanged(visibility: Int) {
super.onWindowVisibilityChanged(visibility) super.onWindowVisibilityChanged(visibility)
if (visibility != android.view.View.VISIBLE) { if (visibility != VISIBLE) {
ghLog("window not visible → shutdownBinding()") ghLog("window not visible → shutdownBinding()")
try { KenjinxNative.detachWindow() } catch (_: Throwable) {} try { KenjinxNative.detachWindow() } catch (_: Throwable) {}
shutdownBinding() shutdownBinding()
@@ -370,7 +370,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
val isSideFlip = (old == 1 && rotation == 3) || (old == 3 && rotation == 1) val isSideFlip = (old == 1 && rotation == 3) || (old == 3 && rotation == 1)
if (isSideFlip) { if (isSideFlip) {
try { KenjinxNative.setSurfaceRotationByAndroidRotation(rotation ?: 0) } catch (_: Throwable) {} try { KenjinxNative.setSurfaceRotationByAndroidRotation(rotation) } catch (_: Throwable) {}
rebindNativeWindow(force = true) rebindNativeWindow(force = true)
val now = android.os.SystemClock.uptimeMillis() val now = android.os.SystemClock.uptimeMillis()
if (now - lastKickAt >= 300L && _inputInitialized && MainActivity.mainViewModel?.rendererReady == true) { if (now - lastKickAt >= 300L && _inputInitialized && MainActivity.mainViewModel?.rendererReady == true) {
@@ -466,12 +466,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
val act = mainViewModel.activity val act = mainViewModel.activity
val intent = Intent(act, EmulationService::class.java) val intent = Intent(act, EmulationService::class.java)
try { try {
if (Build.VERSION.SDK_INT >= 26) { act.startForegroundService(intent)
act.startForegroundService(intent)
} else {
@Suppress("DEPRECATION")
act.startService(intent)
}
} catch (_: Throwable) { } } catch (_: Throwable) { }
try { try {
@@ -1,5 +1,6 @@
package org.kenjinx.android package org.kenjinx.android
import android.os.Build
import com.sun.jna.JNIEnv import com.sun.jna.JNIEnv
import com.sun.jna.Library import com.sun.jna.Library
import com.sun.jna.Native import com.sun.jna.Native
@@ -7,6 +8,7 @@ import org.kenjinx.android.viewmodels.GameInfo
import java.util.Collections import java.util.Collections
import android.view.Surface import android.view.Surface
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
interface KenjinxNativeJna : Library { interface KenjinxNativeJna : Library {
fun deviceInitialize( fun deviceInitialize(
@@ -173,7 +175,7 @@ object KenjinxNative : KenjinxNativeJna by jnaInstance {
backendThreading: Int backendThreading: Int
): Boolean { ): Boolean {
val requested = backendThreading val requested = backendThreading
val isQcom = "qcom".equals(android.os.Build.HARDWARE, true) val isQcom = "qcom".equals(Build.HARDWARE, true)
// Heuristic: On QCOM, when "Auto" is selected, first try single-threaded mode; // Heuristic: On QCOM, when "Auto" is selected, first try single-threaded mode;
// explicitly set values remain unchanged. // explicitly set values remain unchanged.
@@ -182,7 +184,7 @@ object KenjinxNative : KenjinxNativeJna by jnaInstance {
Log.i( Log.i(
"KenjinxNative", "KenjinxNative",
"graphicsInitialize: request=$requested firstChoice=$firstChoice hw=${android.os.Build.HARDWARE}" "graphicsInitialize: request=$requested firstChoice=$firstChoice hw=${Build.HARDWARE}"
) )
return try { return try {
@@ -234,6 +236,7 @@ object KenjinxNative : KenjinxNativeJna by jnaInstance {
} }
// ---------------------------------------------------- // ----------------------------------------------------
@RequiresApi(Build.VERSION_CODES.R)
@JvmStatic @JvmStatic
fun frameEnded() = MainActivity.frameEnded() fun frameEnded() = MainActivity.frameEnded()
@@ -7,7 +7,6 @@ import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.WindowManager import android.view.WindowManager
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
@@ -43,7 +42,10 @@ import androidx.lifecycle.Lifecycle
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.IntentFilter import android.content.IntentFilter
import android.content.Context import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import org.kenjinx.android.service.EmulationService import org.kenjinx.android.service.EmulationService
import androidx.core.content.edit
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
private var physicalControllerManager: PhysicalControllerManager = private var physicalControllerManager: PhysicalControllerManager =
@@ -151,7 +153,7 @@ class MainActivity : BaseActivity() {
override fun run() { override fun run() {
val isReallyResumed = lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) && isActive val isReallyResumed = lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) && isActive
val hasFocusNow = hasWindowFocus() val hasFocusNow = hasWindowFocus()
val rendererReady = MainActivity.mainViewModel?.rendererReady == true val rendererReady = mainViewModel?.rendererReady == true
if (!isReallyResumed || !hasFocusNow || !rendererReady) { if (!isReallyResumed || !hasFocusNow || !rendererReady) {
handler.postDelayed(this, ENABLE_PRESENT_DELAY_MS) handler.postDelayed(this, ENABLE_PRESENT_DELAY_MS)
@@ -231,6 +233,7 @@ class MainActivity : BaseActivity() {
const val EXTRA_TITLE_ID = "titleId" const val EXTRA_TITLE_ID = "titleId"
const val EXTRA_TITLE_NAME = "titleName" const val EXTRA_TITLE_NAME = "titleName"
@RequiresApi(Build.VERSION_CODES.R)
@JvmStatic @JvmStatic
fun frameEnded() { fun frameEnded() {
mainViewModel?.activity?.apply { mainViewModel?.activity?.apply {
@@ -276,6 +279,7 @@ class MainActivity : BaseActivity() {
_isInit = KenjinxNative.javaInitialize(appPath, JNIEnv.CURRENT) _isInit = KenjinxNative.javaInitialize(appPath, JNIEnv.CURRENT)
} }
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ensureNotificationPermission() ensureNotificationPermission()
@@ -283,15 +287,9 @@ class MainActivity : BaseActivity() {
motionSensorManager = MotionSensorManager(this) motionSensorManager = MotionSensorManager(this)
Thread.setDefaultUncaughtExceptionHandler(crashHandler) Thread.setDefaultUncaughtExceptionHandler(crashHandler)
if (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (!Environment.isExternalStorageManager()
!Environment.isExternalStorageManager()
} else {
!Environment.isExternalStorageLegacy()
}
) { ) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { storageHelper?.storage?.requestFullStorageAccess()
storageHelper?.storage?.requestFullStorageAccess()
}
} }
AppPath = this.getExternalFilesDir(null)!!.absolutePath AppPath = this.getExternalFilesDir(null)!!.absolutePath
@@ -335,9 +333,7 @@ class MainActivity : BaseActivity() {
} }
storedIntent = intent storedIntent = intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { rotLog("onCreate: initial display.rotation=${display?.rotation}${deg(display?.rotation)}°")
rotLog("onCreate: initial display.rotation=${display?.rotation}${deg(display?.rotation)}°")
}
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
@@ -379,13 +375,13 @@ class MainActivity : BaseActivity() {
super.onStart() super.onStart()
coldResetIfZombie("onStart") coldResetIfZombie("onStart")
if (isGameRunning && MainActivity.mainViewModel?.rendererReady == true) { if (isGameRunning && mainViewModel?.rendererReady == true) {
try { try {
KenjinxNative.graphicsSetPresentEnabled(true) KenjinxNative.graphicsSetPresentEnabled(true)
Log.d(TAG_FG, "present=ENABLED (onStart)") Log.d(TAG_FG, "present=ENABLED (onStart)")
} catch (_: Throwable) {} } catch (_: Throwable) {}
} else { } else {
Log.d(TAG_FG, "skip enable present (onStart) — rendererReady=${MainActivity.mainViewModel?.rendererReady}") Log.d(TAG_FG, "skip enable present (onStart) — rendererReady=${mainViewModel?.rendererReady}")
setPresentEnabled(false, "cold reset: onStart (no game)") setPresentEnabled(false, "cold reset: onStart (no game)")
} }
} }
@@ -405,15 +401,15 @@ class MainActivity : BaseActivity() {
override fun onTrimMemory(level: Int) { override fun onTrimMemory(level: Int) {
super.onTrimMemory(level) super.onTrimMemory(level)
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN && isGameRunning) { if (level >= TRIM_MEMORY_UI_HIDDEN && isGameRunning) {
setAudioForegroundState(false) setAudioForegroundState(false)
if (MainActivity.mainViewModel?.rendererReady == true) { if (mainViewModel?.rendererReady == true) {
try { try {
setPresentEnabled(false, "onTrimMemory:$level") setPresentEnabled(false, "onTrimMemory:$level")
KenjinxNative.detachWindow() KenjinxNative.detachWindow()
} catch (_: Throwable) {} } catch (_: Throwable) {}
} else { } else {
Log.d(TAG_FG, "skip disable present (onTrimMemory) — rendererReady=${MainActivity.mainViewModel?.rendererReady}") Log.d(TAG_FG, "skip disable present (onTrimMemory) — rendererReady=${mainViewModel?.rendererReady}")
} }
} }
} }
@@ -441,7 +437,7 @@ class MainActivity : BaseActivity() {
registerReceiver( registerReceiver(
serviceStopReceiver, serviceStopReceiver,
IntentFilter(EmulationService.ACTION_STOPPED), IntentFilter(EmulationService.ACTION_STOPPED),
Context.RECEIVER_EXPORTED RECEIVER_EXPORTED
) )
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@@ -495,6 +491,7 @@ class MainActivity : BaseActivity() {
} }
} }
@RequiresApi(Build.VERSION_CODES.R)
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
isActive = false isActive = false
@@ -583,6 +580,7 @@ class MainActivity : BaseActivity() {
try { KenjinxNative.setSurfaceRotationByAndroidRotation(rot) } catch (_: Throwable) {} try { KenjinxNative.setSurfaceRotationByAndroidRotation(rot) } catch (_: Throwable) {}
} }
@RequiresApi(Build.VERSION_CODES.R)
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
val rot = this.display?.rotation val rot = this.display?.rotation
@@ -614,7 +612,7 @@ class MainActivity : BaseActivity() {
) { /* optional: Log/Toast */ } ) { /* optional: Log/Toast */ }
private fun ensureNotificationPermission() { private fun ensureNotificationPermission() {
if (android.os.Build.VERSION.SDK_INT >= 33) { if (Build.VERSION.SDK_INT >= 33) {
val granted = ContextCompat.checkSelfPermission( val granted = ContextCompat.checkSelfPermission(
this, Manifest.permission.POST_NOTIFICATIONS this, Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED ) == PackageManager.PERMISSION_GRANTED
@@ -674,6 +672,7 @@ class MainActivity : BaseActivity() {
} catch (_: Exception) { null } } catch (_: Exception) { null }
} }
@RequiresApi(Build.VERSION_CODES.R)
fun shutdownAndRestart() { fun shutdownAndRestart() {
val packageManager = packageManager val packageManager = packageManager
val intent = packageManager.getLaunchIntentForPackage(packageName) val intent = packageManager.getLaunchIntentForPackage(packageName)
@@ -696,10 +695,10 @@ class MainActivity : BaseActivity() {
private fun setEmuRunningFlag(value: Boolean) { private fun setEmuRunningFlag(value: Boolean) {
try { try {
getSharedPreferences(PREFS, Context.MODE_PRIVATE) getSharedPreferences(PREFS, MODE_PRIVATE)
.edit() .edit {
.putBoolean(KEY_EMU_RUNNING, value) putBoolean(KEY_EMU_RUNNING, value)
.apply() }
} catch (_: Throwable) { } } catch (_: Throwable) { }
} }
@@ -723,7 +722,7 @@ class MainActivity : BaseActivity() {
private fun coldResetIfZombie(phase: String) { private fun coldResetIfZombie(phase: String) {
try { try {
val zombie = getSharedPreferences(PREFS, Context.MODE_PRIVATE) val zombie = getSharedPreferences(PREFS, MODE_PRIVATE)
.getBoolean(KEY_EMU_RUNNING, false) .getBoolean(KEY_EMU_RUNNING, false)
if (zombie) { if (zombie) {
clearEmuRunningFlag() clearEmuRunningFlag()
@@ -1,10 +1,13 @@
package org.kenjinx.android package org.kenjinx.android
import android.content.Intent import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
import kotlin.math.abs import kotlin.math.abs
class PerformanceManager(private val activity: MainActivity) { class PerformanceManager(private val activity: MainActivity) {
companion object { companion object {
@RequiresApi(Build.VERSION_CODES.R)
fun force60HzRefreshRate(enable: Boolean, activity: MainActivity) { fun force60HzRefreshRate(enable: Boolean, activity: MainActivity) {
// Hack for MIUI devices since they don't support the standard Android APIs // Hack for MIUI devices since they don't support the standard Android APIs
try { try {
@@ -25,6 +28,7 @@ class PerformanceManager(private val activity: MainActivity) {
} }
} }
@RequiresApi(Build.VERSION_CODES.R)
fun setTurboMode(enable: Boolean) { fun setTurboMode(enable: Boolean) {
NativeHelpers.instance.setTurboMode(enable) NativeHelpers.instance.setTurboMode(enable)
force60HzRefreshRate(enable, activity) force60HzRefreshRate(enable, activity)
@@ -6,12 +6,12 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Base64 import android.util.Base64
import android.widget.Toast import android.widget.Toast
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
object ShortcutHelper { object ShortcutHelper {
@@ -33,7 +33,7 @@ object ShortcutHelper {
gridIconBitmap: Bitmap? = null, gridIconBitmap: Bitmap? = null,
gridIconBase64: String? = null gridIconBase64: String? = null
) { ) {
val uri = runCatching { Uri.parse(bootPathUri) }.getOrNull() val uri = runCatching { bootPathUri.toUri() }.getOrNull()
// --- Select icon (Grid bitmap > Base64 > App icon) // --- Select icon (Grid bitmap > Base64 > App icon)
var icon = IconCompat.createWithResource(context, R.mipmap.ic_launcher) var icon = IconCompat.createWithResource(context, R.mipmap.ic_launcher)
@@ -1,5 +1,6 @@
package org.kenjinx.android package org.kenjinx.android
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.PendingIntent import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@@ -16,9 +17,8 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.MotionEvent
import android.view.View
@SuppressLint("SourceLockedOrientationActivity")
object ShortcutUtils { object ShortcutUtils {
fun suggestLabelFromUri(uri: Uri): String { fun suggestLabelFromUri(uri: Uri): String {
@@ -47,7 +47,6 @@ object ShortcutUtils {
iconBitmap: Bitmap? = null, iconBitmap: Bitmap? = null,
onCompleted: (() -> Unit)? = null onCompleted: (() -> Unit)? = null
): Boolean { ): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false
val sm = activity.getSystemService(ShortcutManager::class.java) ?: return false val sm = activity.getSystemService(ShortcutManager::class.java) ?: return false
val rw = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION val rw = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
@@ -118,7 +117,7 @@ object ShortcutUtils {
} }
val filter = IntentFilter(ACTION_PIN_RESULT) val filter = IntentFilter(ACTION_PIN_RESULT)
if (Build.VERSION.SDK_INT >= 33) { if (Build.VERSION.SDK_INT >= 33) {
activity.registerReceiver(pinResultReceiver!!, filter, Context.RECEIVER_NOT_EXPORTED) activity.registerReceiver(pinResultReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else { } else {
@Suppress("UnspecifiedRegisterReceiverFlag") @Suppress("UnspecifiedRegisterReceiverFlag")
activity.registerReceiver(pinResultReceiver, filter) activity.registerReceiver(pinResultReceiver, filter)
@@ -1,5 +1,6 @@
package org.kenjinx.android package org.kenjinx.android
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Intent import android.content.Intent
@@ -14,6 +15,7 @@ import android.util.Base64
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import org.kenjinx.android.viewmodels.GameInfo import org.kenjinx.android.viewmodels.GameInfo
@SuppressLint("SourceLockedOrientationActivity")
class ShortcutWizardActivity : Activity() { class ShortcutWizardActivity : Activity() {
private val REQ_PICK_GAME = 1 private val REQ_PICK_GAME = 1
@@ -100,7 +100,7 @@ class UiHandler {
if (inputText.value.isEmpty()) { if (inputText.value.isEmpty()) {
validation.value = "Must be between $minLength and $maxLength characters" validation.value = "Must be between $minLength and $maxLength characters"
} else { } else {
return inputText.value.length < minLength || inputText.value.length > maxLength return inputText.value.length !in minLength..maxLength
} }
return false return false
} }
@@ -116,7 +116,7 @@ class UiHandler {
fun submit() { fun submit() {
if (type == 2) { if (type == 2) {
if (inputListener.value.length < minLength || inputListener.value.length > maxLength) return if (inputListener.value.length !in minLength..maxLength) return
} }
KenjinxNative.uiHandlerSetResponse(true, if (type == 2) inputListener.value else "") KenjinxNative.uiHandlerSetResponse(true, if (type == 2) inputListener.value else "")
showMessageListener.value = false showMessageListener.value = false
@@ -30,7 +30,7 @@ private fun allCheatDirs(activity: Activity, titleId: String): List<File> {
private fun parseCheatNames(text: String): List<String> { private fun parseCheatNames(text: String): List<String> {
// Trim BOM, CRLF tolerant // Trim BOM, CRLF tolerant
val clean = text.replace("\uFEFF", "") val clean = text.replace("\uFEFF", "")
val rx = Regex("""(?m)^\s*\[(.+?)\]\s*$""") val rx = Regex("""(?m)^\s*\[(.+?)]\s*$""")
return rx.findAll(clean) return rx.findAll(clean)
.map { it.groupValues[1].trim() } .map { it.groupValues[1].trim() }
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
@@ -1,6 +1,7 @@
package org.kenjinx.android.cheats package org.kenjinx.android.cheats
import android.content.Context import android.content.Context
import androidx.core.content.edit
class CheatPrefs(private val context: Context) { class CheatPrefs(private val context: Context) {
private fun key(titleId: String) = "cheats_$titleId" private fun key(titleId: String) = "cheats_$titleId"
@@ -11,6 +12,6 @@ class CheatPrefs(private val context: Context) {
} }
fun setEnabled(titleId: String, keys: Set<String>) { fun setEnabled(titleId: String, keys: Set<String>) {
prefs.edit().putStringSet(key(titleId), keys.toSet()).apply() prefs.edit { putStringSet(key(titleId), keys.toSet()) }
} }
} }
@@ -6,8 +6,6 @@ import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.util.Log import android.util.Log
import java.io.File import java.io.File
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
/* -------- Paths -------- */ /* -------- Paths -------- */
@@ -246,7 +246,7 @@ fun importSaveFromZip(
} }
if (titleIdFromZip == null) return ImportResult(false, "error importing save. missing TITLEID folder") if (titleIdFromZip == null) return ImportResult(false, "error importing save. missing TITLEID folder")
val tidZip = titleIdFromZip!!.lowercase(Locale.ROOT) val tidZip = titleIdFromZip.lowercase(Locale.ROOT)
// Sum the size only under /<TITLEID>/… // Sum the size only under /<TITLEID>/…
runCatching { runCatching {
@@ -5,10 +5,8 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Binder import android.os.Binder
import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import org.kenjinx.android.MainActivity import org.kenjinx.android.MainActivity
@@ -138,10 +136,7 @@ class EmulationService : Service() {
private fun buildNotification(): Notification { private fun buildNotification(): Notification {
val openIntent = Intent(this, MainActivity::class.java) val openIntent = Intent(this, MainActivity::class.java)
val flags = if (Build.VERSION.SDK_INT >= 23) val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
else
PendingIntent.FLAG_UPDATE_CURRENT
val pi = PendingIntent.getActivity(this, 0, openIntent, flags) val pi = PendingIntent.getActivity(this, 0, openIntent, flags)
return NotificationCompat.Builder(this, CHANNEL_ID) return NotificationCompat.Builder(this, CHANNEL_ID)
@@ -156,15 +151,13 @@ class EmulationService : Service() {
} }
private fun createNotificationChannel() { private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= 26) { val mgr = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val mgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val ch = NotificationChannel(
val ch = NotificationChannel( CHANNEL_ID,
CHANNEL_ID, "Emulation",
"Emulation", NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_LOW )
) ch.description = "Keeps the Emulation active."
ch.description = "Keeps the Emulation active." mgr.createNotificationChannel(ch)
mgr.createNotificationChannel(ch)
}
} }
} }
@@ -212,11 +212,9 @@ class DlcViewModel(val titleId: String) {
} }
var data: MutableList<DlcContainerList>? = null var data: MutableList<DlcContainerList>? = null
private var jsonPath: String private var jsonPath: String = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + "/dlc.json"
init { init {
jsonPath =
MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + "/dlc.json"
storageHelper = MainActivity.StorageHelper!! storageHelper = MainActivity.StorageHelper!!
reloadFromDisk() reloadFromDisk()
@@ -1,5 +1,6 @@
package org.kenjinx.android.viewmodels package org.kenjinx.android.viewmodels
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
@@ -10,10 +11,15 @@ import org.kenjinx.android.KenjinxNative
class GameModel(var file: DocumentFile, val context: Context) { class GameModel(var file: DocumentFile, val context: Context) {
private var updateDescriptor: ParcelFileDescriptor? = null private var updateDescriptor: ParcelFileDescriptor? = null
var type: FileType var type: FileType = when {
(file.extension == "xci") -> FileType.Xci
(file.extension == "nsp") -> FileType.Nsp
(file.extension == "nro") -> FileType.Nro
else -> FileType.None
}
var descriptor: ParcelFileDescriptor? = null var descriptor: ParcelFileDescriptor? = null
var fileName: String? var fileName = file.name
var fileBaseName: String? var fileBaseName: String? = fileName?.substringBeforeLast(".", fileName!!)
var fileSize = 0.0 var fileSize = 0.0
var titleName: String? = null var titleName: String? = null
var titleId: String? = null var titleId: String? = null
@@ -21,17 +27,6 @@ class GameModel(var file: DocumentFile, val context: Context) {
var version: String? = null var version: String? = null
var icon: String? = null var icon: String? = null
init {
fileName = file.name
fileBaseName = fileName?.substringBeforeLast(".", fileName!!)
type = when {
(file.extension == "xci") -> FileType.Xci
(file.extension == "nsp") -> FileType.Nsp
(file.extension == "nro") -> FileType.Nro
else -> FileType.None
}
}
fun getGameInfo() { fun getGameInfo() {
val pid = open() val pid = open()
val gameInfo = GameInfo() val gameInfo = GameInfo()
@@ -60,6 +55,7 @@ class GameModel(var file: DocumentFile, val context: Context) {
return descriptor?.fd ?: 0 return descriptor?.fd ?: 0
} }
@SuppressLint("UseKtx")
fun openUpdate(): Int { fun openUpdate(): Int {
if (titleId?.isNotEmpty() == true) { if (titleId?.isNotEmpty() == true) {
val vm = TitleUpdateViewModel(titleId ?: "") val vm = TitleUpdateViewModel(titleId ?: "")
@@ -27,6 +27,8 @@ import org.kenjinx.android.UiHandler
import android.util.Log import android.util.Log
import java.io.File import java.io.File
import java.util.TimeZone import java.util.TimeZone
import androidx.core.content.edit
import androidx.core.net.toUri
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
class MainViewModel(val activity: MainActivity) { class MainViewModel(val activity: MainActivity) {
@@ -62,7 +64,7 @@ class MainViewModel(val activity: MainActivity) {
set(value) { set(value) {
field = value field = value
val prefs = PreferenceManager.getDefaultSharedPreferences(activity) val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
prefs.edit().putString("defaultGameFolderUri", value?.toString() ?: "").apply() prefs.edit { putString("defaultGameFolderUri", value?.toString() ?: "") }
} }
var gameHost: GameHost? = null var gameHost: GameHost? = null
set(value) { set(value) {
@@ -79,7 +81,7 @@ class MainViewModel(val activity: MainActivity) {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity) val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
val saved = prefs.getString("defaultGameFolderUri", "") ?: "" val saved = prefs.getString("defaultGameFolderUri", "") ?: ""
if (saved.isNotEmpty()) { if (saved.isNotEmpty()) {
defaultGameFolderUri = Uri.parse(saved) defaultGameFolderUri = saved.toUri()
} }
} }
@@ -16,7 +16,7 @@ import java.io.File
class TitleUpdateViewModel(val titleId: String) { class TitleUpdateViewModel(val titleId: String) {
private var canClose: MutableState<Boolean>? = null private var canClose: MutableState<Boolean>? = null
private var basePath: String private var basePath: String = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
private var updateJsonName = "updates.json" private var updateJsonName = "updates.json"
private var storageHelper: SimpleStorageHelper private var storageHelper: SimpleStorageHelper
private var currentPaths: MutableList<String> = mutableListOf() private var currentPaths: MutableList<String> = mutableListOf()
@@ -187,11 +187,9 @@ class TitleUpdateViewModel(val titleId: String) {
} }
var data: TitleUpdateMetadata? = null var data: TitleUpdateMetadata? = null
private var jsonPath: String private var jsonPath: String = "${basePath}/${updateJsonName}"
init { init {
basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
jsonPath = "${basePath}/${updateJsonName}"
data = TitleUpdateMetadata() data = TitleUpdateMetadata()
if (File(jsonPath).exists()) { if (File(jsonPath).exists()) {
@@ -53,7 +53,6 @@ import org.kenjinx.android.viewmodels.VSyncMode
import org.kenjinx.android.widgets.SimpleAlertDialog import org.kenjinx.android.widgets.SimpleAlertDialog
import java.util.Locale import java.util.Locale
import kotlin.math.roundToInt import kotlin.math.roundToInt
import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -61,6 +60,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import org.kenjinx.android.viewmodels.QuickSettings.VirtualControllerPreset import org.kenjinx.android.viewmodels.QuickSettings.VirtualControllerPreset
import androidx.core.net.toUri
class GameViews { class GameViews {
@@ -338,7 +338,7 @@ class GameViews {
Toast.makeText(mainViewModel.activity, "Slot 1 is empty.", Toast.LENGTH_SHORT).show() Toast.makeText(mainViewModel.activity, "Slot 1 is empty.", Toast.LENGTH_SHORT).show()
} else { } else {
try { try {
val bytes = mainViewModel.activity.contentResolver.openInputStream(Uri.parse(u))?.use { it.readBytes() } val bytes = mainViewModel.activity.contentResolver.openInputStream(u.toUri())?.use { it.readBytes() }
if (bytes != null && bytes.isNotEmpty()) { if (bytes != null && bytes.isNotEmpty()) {
val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size) val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size)
if (ok) Toast.makeText(mainViewModel.activity, "Loaded: $name", Toast.LENGTH_SHORT).show() if (ok) Toast.makeText(mainViewModel.activity, "Loaded: $name", Toast.LENGTH_SHORT).show()
@@ -346,7 +346,7 @@ class GameViews {
} else Toast.makeText(mainViewModel.activity, "File not readable.", Toast.LENGTH_SHORT).show() } else Toast.makeText(mainViewModel.activity, "File not readable.", Toast.LENGTH_SHORT).show()
} catch (t: Throwable) { Toast.makeText(mainViewModel.activity, "Error: ${t.message}", Toast.LENGTH_SHORT).show() } } catch (t: Throwable) { Toast.makeText(mainViewModel.activity, "Error: ${t.message}", Toast.LENGTH_SHORT).show() }
} }
}) { androidx.compose.material3.Text(qsLabel(QuickSettings(mainViewModel.activity).amiibo1Name, 1)) } }) { Text(qsLabel(QuickSettings(mainViewModel.activity).amiibo1Name, 1)) }
androidx.compose.material3.Button(onClick = { androidx.compose.material3.Button(onClick = {
val qs = QuickSettings(mainViewModel.activity) val qs = QuickSettings(mainViewModel.activity)
@@ -356,7 +356,7 @@ class GameViews {
Toast.makeText(mainViewModel.activity, "Slot 2 is empty.", Toast.LENGTH_SHORT).show() Toast.makeText(mainViewModel.activity, "Slot 2 is empty.", Toast.LENGTH_SHORT).show()
} else { } else {
try { try {
val bytes = mainViewModel.activity.contentResolver.openInputStream(Uri.parse(u))?.use { it.readBytes() } val bytes = mainViewModel.activity.contentResolver.openInputStream(u.toUri())?.use { it.readBytes() }
if (bytes != null && bytes.isNotEmpty()) { if (bytes != null && bytes.isNotEmpty()) {
val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size) val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size)
if (ok) Toast.makeText(mainViewModel.activity, "Loaded: $name", Toast.LENGTH_SHORT).show() if (ok) Toast.makeText(mainViewModel.activity, "Loaded: $name", Toast.LENGTH_SHORT).show()
@@ -364,7 +364,7 @@ class GameViews {
} else Toast.makeText(mainViewModel.activity, "File not readable.", Toast.LENGTH_SHORT).show() } else Toast.makeText(mainViewModel.activity, "File not readable.", Toast.LENGTH_SHORT).show()
} catch (t: Throwable) { Toast.makeText(mainViewModel.activity, "Error: ${t.message}", Toast.LENGTH_SHORT).show() } } catch (t: Throwable) { Toast.makeText(mainViewModel.activity, "Error: ${t.message}", Toast.LENGTH_SHORT).show() }
} }
}) { androidx.compose.material3.Text(qsLabel(QuickSettings(mainViewModel.activity).amiibo2Name, 2)) } }) { Text(qsLabel(QuickSettings(mainViewModel.activity).amiibo2Name, 2)) }
androidx.compose.material3.Button(onClick = { androidx.compose.material3.Button(onClick = {
val qs = QuickSettings(mainViewModel.activity) val qs = QuickSettings(mainViewModel.activity)
@@ -374,7 +374,7 @@ class GameViews {
Toast.makeText(mainViewModel.activity, "Slot 3 is empty.", Toast.LENGTH_SHORT).show() Toast.makeText(mainViewModel.activity, "Slot 3 is empty.", Toast.LENGTH_SHORT).show()
} else { } else {
try { try {
val bytes = mainViewModel.activity.contentResolver.openInputStream(Uri.parse(u))?.use { it.readBytes() } val bytes = mainViewModel.activity.contentResolver.openInputStream(u.toUri())?.use { it.readBytes() }
if (bytes != null && bytes.isNotEmpty()) { if (bytes != null && bytes.isNotEmpty()) {
val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size) val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size)
if (ok) Toast.makeText(mainViewModel.activity, "Loaded: $name", Toast.LENGTH_SHORT).show() if (ok) Toast.makeText(mainViewModel.activity, "Loaded: $name", Toast.LENGTH_SHORT).show()
@@ -382,7 +382,7 @@ class GameViews {
} else Toast.makeText(mainViewModel.activity, "File not readable.", Toast.LENGTH_SHORT).show() } else Toast.makeText(mainViewModel.activity, "File not readable.", Toast.LENGTH_SHORT).show()
} catch (t: Throwable) { Toast.makeText(mainViewModel.activity, "Error: ${t.message}", Toast.LENGTH_SHORT).show() } } catch (t: Throwable) { Toast.makeText(mainViewModel.activity, "Error: ${t.message}", Toast.LENGTH_SHORT).show() }
} }
}) { androidx.compose.material3.Text(qsLabel(QuickSettings(mainViewModel.activity).amiibo3Name, 3)) } }) { Text(qsLabel(QuickSettings(mainViewModel.activity).amiibo3Name, 3)) }
} }
Row(modifier = Modifier.padding(top = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(modifier = Modifier.padding(top = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
@@ -394,7 +394,7 @@ class GameViews {
Toast.makeText(mainViewModel.activity, "Slot 4 is empty.", Toast.LENGTH_SHORT).show() Toast.makeText(mainViewModel.activity, "Slot 4 is empty.", Toast.LENGTH_SHORT).show()
} else { } else {
try { try {
val bytes = mainViewModel.activity.contentResolver.openInputStream(Uri.parse(u))?.use { it.readBytes() } val bytes = mainViewModel.activity.contentResolver.openInputStream(u.toUri())?.use { it.readBytes() }
if (bytes != null && bytes.isNotEmpty()) { if (bytes != null && bytes.isNotEmpty()) {
val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size) val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size)
if (ok) Toast.makeText(mainViewModel.activity, "Loaded: $name", Toast.LENGTH_SHORT).show() if (ok) Toast.makeText(mainViewModel.activity, "Loaded: $name", Toast.LENGTH_SHORT).show()
@@ -402,7 +402,7 @@ class GameViews {
} else Toast.makeText(mainViewModel.activity, "File not readable.", Toast.LENGTH_SHORT).show() } else Toast.makeText(mainViewModel.activity, "File not readable.", Toast.LENGTH_SHORT).show()
} catch (t: Throwable) { Toast.makeText(mainViewModel.activity, "Error: ${t.message}", Toast.LENGTH_SHORT).show() } } catch (t: Throwable) { Toast.makeText(mainViewModel.activity, "Error: ${t.message}", Toast.LENGTH_SHORT).show() }
} }
}) { androidx.compose.material3.Text(qsLabel(QuickSettings(mainViewModel.activity).amiibo4Name, 4)) } }) { Text(qsLabel(QuickSettings(mainViewModel.activity).amiibo4Name, 4)) }
androidx.compose.material3.Button(onClick = { androidx.compose.material3.Button(onClick = {
val qs = QuickSettings(mainViewModel.activity) val qs = QuickSettings(mainViewModel.activity)
@@ -412,7 +412,7 @@ class GameViews {
Toast.makeText(mainViewModel.activity, "Slot 5 is empty.", Toast.LENGTH_SHORT).show() Toast.makeText(mainViewModel.activity, "Slot 5 is empty.", Toast.LENGTH_SHORT).show()
} else { } else {
try { try {
val bytes = mainViewModel.activity.contentResolver.openInputStream(Uri.parse(u))?.use { it.readBytes() } val bytes = mainViewModel.activity.contentResolver.openInputStream(u.toUri())?.use { it.readBytes() }
if (bytes != null && bytes.isNotEmpty()) { if (bytes != null && bytes.isNotEmpty()) {
val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size) val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size)
if (ok) Toast.makeText(mainViewModel.activity, "Loaded: $name", Toast.LENGTH_SHORT).show() if (ok) Toast.makeText(mainViewModel.activity, "Loaded: $name", Toast.LENGTH_SHORT).show()
@@ -420,12 +420,12 @@ class GameViews {
} else Toast.makeText(mainViewModel.activity, "File not readable.", Toast.LENGTH_SHORT).show() } else Toast.makeText(mainViewModel.activity, "File not readable.", Toast.LENGTH_SHORT).show()
} catch (t: Throwable) { Toast.makeText(mainViewModel.activity, "Error: ${t.message}", Toast.LENGTH_SHORT).show() } } catch (t: Throwable) { Toast.makeText(mainViewModel.activity, "Error: ${t.message}", Toast.LENGTH_SHORT).show() }
} }
}) { androidx.compose.material3.Text(qsLabel(QuickSettings(mainViewModel.activity).amiibo5Name, 5)) } }) { Text(qsLabel(QuickSettings(mainViewModel.activity).amiibo5Name, 5)) }
androidx.compose.material3.OutlinedButton(onClick = { androidx.compose.material3.OutlinedButton(onClick = {
KenjinxNative.amiiboClear() KenjinxNative.amiiboClear()
Toast.makeText(mainViewModel.activity, "Amiibo cleared", Toast.LENGTH_SHORT).show() Toast.makeText(mainViewModel.activity, "Amiibo cleared", Toast.LENGTH_SHORT).show()
}) { androidx.compose.material3.Text("Clear") } }) { Text("Clear") }
} }
} }
} }
@@ -92,12 +92,12 @@ import org.kenjinx.android.viewmodels.HomeViewModel
import org.kenjinx.android.viewmodels.QuickSettings import org.kenjinx.android.viewmodels.QuickSettings
import org.kenjinx.android.widgets.SimpleAlertDialog import org.kenjinx.android.widgets.SimpleAlertDialog
import org.kenjinx.android.ShortcutUtils import org.kenjinx.android.ShortcutUtils
import org.kenjinx.android.ShortcutWizardActivity
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.mutableFloatStateOf
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
// Cheats // Cheats
@@ -162,19 +162,19 @@ class HomeViews {
var isFabVisible by remember { mutableStateOf(true) } var isFabVisible by remember { mutableStateOf(true) }
val isNavigating = remember { mutableStateOf(false) } val isNavigating = remember { mutableStateOf(false) }
// NEW: Amiibo slot picker state // Amiibo slot picker state
val showAmiiboSlotDialog = remember { mutableStateOf(false) } val showAmiiboSlotDialog = remember { mutableStateOf(false) }
val pendingSlot = remember { mutableStateOf(1) } val pendingSlot = remember { mutableStateOf(1) }
// NEW: Cheats UI state // Cheats UI state
val openCheatsDialog = remember { mutableStateOf(false) } val openCheatsDialog = remember { mutableStateOf(false) }
val cheatsForSelected = remember { mutableStateOf(listOf<CheatItem>()) } val cheatsForSelected = remember { mutableStateOf(listOf<CheatItem>()) }
val enabledCheatKeys = remember { mutableStateOf(mutableSetOf<String>()) } val enabledCheatKeys = remember { mutableStateOf(mutableSetOf<String>()) }
// NEW: Mods UI state // Mods UI state
val openModsDialog = remember { mutableStateOf(false) } val openModsDialog = remember { mutableStateOf(false) }
val modsForSelected = remember { mutableStateOf(listOf<String>()) } val modsForSelected = remember { mutableStateOf(listOf<String>()) }
val modsImportProgress = remember { mutableStateOf(0f) } val modsImportProgress = remember { mutableFloatStateOf(0f) }
val modsImportBusy = remember { mutableStateOf(false) } val modsImportBusy = remember { mutableStateOf(false) }
val modsImportStatusText = remember { mutableStateOf("") } val modsImportStatusText = remember { mutableStateOf("") }
@@ -182,8 +182,8 @@ class HomeViews {
val openSavesDialog = remember { mutableStateOf(false) } val openSavesDialog = remember { mutableStateOf(false) }
val saveImportBusy = remember { mutableStateOf(false) } val saveImportBusy = remember { mutableStateOf(false) }
val saveExportBusy = remember { mutableStateOf(false) } val saveExportBusy = remember { mutableStateOf(false) }
val saveImportProgress = remember { mutableStateOf(0f) } val saveImportProgress = remember { mutableFloatStateOf(0f) }
val saveExportProgress = remember { mutableStateOf(0f) } val saveExportProgress = remember { mutableFloatStateOf(0f) }
val saveImportStatus = remember { mutableStateOf("") } val saveImportStatus = remember { mutableStateOf("") }
val saveExportStatus = remember { mutableStateOf("") } val saveExportStatus = remember { mutableStateOf("") }
@@ -198,13 +198,13 @@ class HomeViews {
val tIdNow = viewModel.mainViewModel?.selected?.titleId.orEmpty() val tIdNow = viewModel.mainViewModel?.selected?.titleId.orEmpty()
if (uri != null && act != null && tIdNow.isNotEmpty()) { if (uri != null && act != null && tIdNow.isNotEmpty()) {
saveImportBusy.value = true saveImportBusy.value = true
saveImportProgress.value = 0f saveImportProgress.floatValue = 0f
saveImportStatus.value = "Starting…" saveImportStatus.value = "Starting…"
thread { thread {
val res = importSaveFromZip(act, uri) { prog -> val res = importSaveFromZip(act, uri) { prog ->
val frac = if (prog.total > 0) prog.bytes.toFloat() / prog.total else 0f val frac = if (prog.total > 0) prog.bytes.toFloat() / prog.total else 0f
saveImportProgress.value = frac.coerceIn(0f, 1f) saveImportProgress.floatValue = frac.coerceIn(0f, 1f)
saveImportStatus.value = "Importing: ${prog.currentEntry}" saveImportStatus.value = "Importing: ${prog.currentEntry}"
} }
saveImportBusy.value = false saveImportBusy.value = false
@@ -223,13 +223,13 @@ class HomeViews {
val tIdNow = viewModel.mainViewModel?.selected?.titleId.orEmpty() val tIdNow = viewModel.mainViewModel?.selected?.titleId.orEmpty()
if (uri != null && act != null && tIdNow.isNotEmpty()) { if (uri != null && act != null && tIdNow.isNotEmpty()) {
saveExportBusy.value = true saveExportBusy.value = true
saveExportProgress.value = 0f saveExportProgress.floatValue = 0f
saveExportStatus.value = "Starting…" saveExportStatus.value = "Starting…"
thread { thread {
val res = exportSaveToZip(act, tIdNow, uri) { prog -> val res = exportSaveToZip(act, tIdNow, uri) { prog ->
val frac = if (prog.total > 0) prog.bytes.toFloat() / prog.total else 0f val frac = if (prog.total > 0) prog.bytes.toFloat() / prog.total else 0f
saveExportProgress.value = frac.coerceIn(0f, 1f) saveExportProgress.floatValue = frac.coerceIn(0f, 1f)
saveExportStatus.value = "Exporting: ${prog.currentPath}" saveExportStatus.value = "Exporting: ${prog.currentPath}"
} }
saveExportBusy.value = false saveExportBusy.value = false
@@ -250,7 +250,7 @@ class HomeViews {
val context = LocalContext.current val context = LocalContext.current
// NEW: Amiibo Launcher (OpenDocument) // Amiibo Launcher (OpenDocument)
val pickAmiiboLauncher = rememberLauncherForActivityResult( val pickAmiiboLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument() contract = ActivityResultContracts.OpenDocument()
) { uri: Uri? -> ) { uri: Uri? ->
@@ -275,7 +275,7 @@ class HomeViews {
} }
} }
// NEW: Cheats Import (.txt) // Cheats Import (.txt)
val importCheatLauncher = rememberLauncherForActivityResult( val importCheatLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument() contract = ActivityResultContracts.OpenDocument()
) { uri: Uri? -> ) { uri: Uri? ->
@@ -316,7 +316,7 @@ class HomeViews {
} catch (_: Exception) {} } catch (_: Exception) {}
modsImportBusy.value = true modsImportBusy.value = true
modsImportProgress.value = 0f modsImportProgress.floatValue = 0f
modsImportStatusText.value = "Starting…" modsImportStatusText.value = "Starting…"
thread { thread {
@@ -325,7 +325,7 @@ class HomeViews {
titleId, titleId,
uri uri
) { prog -> ) { prog ->
modsImportProgress.value = prog.fraction modsImportProgress.floatValue = prog.fraction
modsImportStatusText.value = if (prog.currentEntry.isNotEmpty()) modsImportStatusText.value = if (prog.currentEntry.isNotEmpty())
"Copying: ${prog.currentEntry}" "Copying: ${prog.currentEntry}"
else else
@@ -642,7 +642,7 @@ class HomeViews {
// Amiibo slot chooser dialog (outside of game) // Amiibo slot chooser dialog (outside of game)
if (showAmiiboSlotDialog.value) { if (showAmiiboSlotDialog.value) {
androidx.compose.material3.AlertDialog( AlertDialog(
onDismissRequest = { showAmiiboSlotDialog.value = false }, onDismissRequest = { showAmiiboSlotDialog.value = false },
title = { Text("Import Amiibo") }, title = { Text("Import Amiibo") },
text = { text = {
@@ -732,7 +732,7 @@ class HomeViews {
if (viewModel.mainViewModel?.selected != null) { if (viewModel.mainViewModel?.selected != null) {
// Press the cheat buttons before starting (Run button). // Press the cheat buttons before starting (Run button).
val gmSel = viewModel.mainViewModel!!.selected!! val gmSel = viewModel.mainViewModel.selected!!
val tId = gmSel.titleId ?: "" val tId = gmSel.titleId ?: ""
val act = viewModel.activity val act = viewModel.activity
@@ -1125,7 +1125,7 @@ class HomeViews {
.padding(top = 12.dp), .padding(top = 12.dp),
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
) { ) {
androidx.compose.material3.TextButton( TextButton(
onClick = { openSavesDialog.value = false } onClick = { openSavesDialog.value = false }
) { Text("Close") } ) { Text("Close") }
} }