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"
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>
<!-- 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"/>
<!-- Allgemein: LAUNCHER-Activities und ACTION_VIEW-Dateien auflösbar machen -->
<!-- General: Make LAUNCHER activities and ACTION_VIEW files resolvable -->
<intent>
<action android:name="android.intent.action.MAIN" />
<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.CrossContentDescription
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.event.Event
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) ---
private fun generateConfig2(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig {
private fun generateConfig2(isLeft: Boolean, useSwitchLayout: Boolean): GamePadConfig {
val distance = 0.3f
val buttonScale = 1f
if (isLeft) {
return RadialGamePadConfig(
return GamePadConfig(
/* ringSegments = */ 12,
/* Primary (Stick) */
// 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 {
return RadialGamePadConfig(
return GamePadConfig(
/* ringSegments = */ 12,
/* Primary (ABXY) */
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.CrossContentDescription
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.event.Event
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 buttonScale = 1f
if (isLeft) {
return RadialGamePadConfig(
return GamePadConfig(
/* ringSegments = */ 12,
/* Primary (D-Pad) */
PrimaryDialConfig.Cross(
@@ -293,7 +292,7 @@ private fun generateConfig3(isLeft: Boolean, useSwitchLayout: Boolean): RadialGa
)
)
} else {
return RadialGamePadConfig(
return GamePadConfig(
/* ringSegments = */ 12,
/* Primary (ABXY) */
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.CrossContentDescription
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.event.Event
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 buttonScale = 1f
if (isLeft) {
return RadialGamePadConfig(
return GamePadConfig(
/* ringSegments = */ 12,
/* Primary (Stick) */
// 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 {
return RadialGamePadConfig(
return GamePadConfig(
/* ringSegments = */ 12,
/* Primary (ABXY) */
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.CrossContentDescription
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.event.Event
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 buttonScale = 1f
if (isLeft) {
return RadialGamePadConfig(
return GamePadConfig(
/* ringSegments = */ 12,
/* Primary (Stick) */
// 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 {
return RadialGamePadConfig(
return GamePadConfig(
/* ringSegments = */ 12,
/* Primary (ABXY) */
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.CrossContentDescription
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.event.Event
import kotlinx.coroutines.flow.merge
@@ -177,7 +177,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
override fun onWindowVisibilityChanged(visibility: Int) {
super.onWindowVisibilityChanged(visibility)
if (visibility != android.view.View.VISIBLE) {
if (visibility != VISIBLE) {
ghLog("window not visible → shutdownBinding()")
try { KenjinxNative.detachWindow() } catch (_: Throwable) {}
shutdownBinding()
@@ -370,7 +370,7 @@ class GameHost(context: Context?, private val mainViewModel: MainViewModel) : Su
val isSideFlip = (old == 1 && rotation == 3) || (old == 3 && rotation == 1)
if (isSideFlip) {
try { KenjinxNative.setSurfaceRotationByAndroidRotation(rotation ?: 0) } catch (_: Throwable) {}
try { KenjinxNative.setSurfaceRotationByAndroidRotation(rotation) } catch (_: Throwable) {}
rebindNativeWindow(force = true)
val now = android.os.SystemClock.uptimeMillis()
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 intent = Intent(act, EmulationService::class.java)
try {
if (Build.VERSION.SDK_INT >= 26) {
act.startForegroundService(intent)
} else {
@Suppress("DEPRECATION")
act.startService(intent)
}
} catch (_: Throwable) { }
try {
@@ -1,5 +1,6 @@
package org.kenjinx.android
import android.os.Build
import com.sun.jna.JNIEnv
import com.sun.jna.Library
import com.sun.jna.Native
@@ -7,6 +8,7 @@ import org.kenjinx.android.viewmodels.GameInfo
import java.util.Collections
import android.view.Surface
import android.util.Log
import androidx.annotation.RequiresApi
interface KenjinxNativeJna : Library {
fun deviceInitialize(
@@ -173,7 +175,7 @@ object KenjinxNative : KenjinxNativeJna by jnaInstance {
backendThreading: Int
): Boolean {
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;
// explicitly set values remain unchanged.
@@ -182,7 +184,7 @@ object KenjinxNative : KenjinxNativeJna by jnaInstance {
Log.i(
"KenjinxNative",
"graphicsInitialize: request=$requested firstChoice=$firstChoice hw=${android.os.Build.HARDWARE}"
"graphicsInitialize: request=$requested firstChoice=$firstChoice hw=${Build.HARDWARE}"
)
return try {
@@ -234,6 +236,7 @@ object KenjinxNative : KenjinxNativeJna by jnaInstance {
}
// ----------------------------------------------------
@RequiresApi(Build.VERSION_CODES.R)
@JvmStatic
fun frameEnded() = MainActivity.frameEnded()
@@ -7,7 +7,6 @@ import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
@@ -43,7 +42,10 @@ import androidx.lifecycle.Lifecycle
import android.content.BroadcastReceiver
import android.content.IntentFilter
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import org.kenjinx.android.service.EmulationService
import androidx.core.content.edit
class MainActivity : BaseActivity() {
private var physicalControllerManager: PhysicalControllerManager =
@@ -151,7 +153,7 @@ class MainActivity : BaseActivity() {
override fun run() {
val isReallyResumed = lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) && isActive
val hasFocusNow = hasWindowFocus()
val rendererReady = MainActivity.mainViewModel?.rendererReady == true
val rendererReady = mainViewModel?.rendererReady == true
if (!isReallyResumed || !hasFocusNow || !rendererReady) {
handler.postDelayed(this, ENABLE_PRESENT_DELAY_MS)
@@ -231,6 +233,7 @@ class MainActivity : BaseActivity() {
const val EXTRA_TITLE_ID = "titleId"
const val EXTRA_TITLE_NAME = "titleName"
@RequiresApi(Build.VERSION_CODES.R)
@JvmStatic
fun frameEnded() {
mainViewModel?.activity?.apply {
@@ -276,6 +279,7 @@ class MainActivity : BaseActivity() {
_isInit = KenjinxNative.javaInitialize(appPath, JNIEnv.CURRENT)
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ensureNotificationPermission()
@@ -283,16 +287,10 @@ class MainActivity : BaseActivity() {
motionSensorManager = MotionSensorManager(this)
Thread.setDefaultUncaughtExceptionHandler(crashHandler)
if (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
!Environment.isExternalStorageManager()
} else {
!Environment.isExternalStorageLegacy()
}
if (!Environment.isExternalStorageManager()
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
storageHelper?.storage?.requestFullStorageAccess()
}
}
AppPath = this.getExternalFilesDir(null)!!.absolutePath
@@ -335,10 +333,8 @@ class MainActivity : BaseActivity() {
}
storedIntent = intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
rotLog("onCreate: initial display.rotation=${display?.rotation}${deg(display?.rotation)}°")
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
@@ -379,13 +375,13 @@ class MainActivity : BaseActivity() {
super.onStart()
coldResetIfZombie("onStart")
if (isGameRunning && MainActivity.mainViewModel?.rendererReady == true) {
if (isGameRunning && mainViewModel?.rendererReady == true) {
try {
KenjinxNative.graphicsSetPresentEnabled(true)
Log.d(TAG_FG, "present=ENABLED (onStart)")
} catch (_: Throwable) {}
} 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)")
}
}
@@ -405,15 +401,15 @@ class MainActivity : BaseActivity() {
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN && isGameRunning) {
if (level >= TRIM_MEMORY_UI_HIDDEN && isGameRunning) {
setAudioForegroundState(false)
if (MainActivity.mainViewModel?.rendererReady == true) {
if (mainViewModel?.rendererReady == true) {
try {
setPresentEnabled(false, "onTrimMemory:$level")
KenjinxNative.detachWindow()
} catch (_: Throwable) {}
} 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(
serviceStopReceiver,
IntentFilter(EmulationService.ACTION_STOPPED),
Context.RECEIVER_EXPORTED
RECEIVER_EXPORTED
)
} else {
@Suppress("DEPRECATION")
@@ -495,6 +491,7 @@ class MainActivity : BaseActivity() {
}
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onPause() {
super.onPause()
isActive = false
@@ -583,6 +580,7 @@ class MainActivity : BaseActivity() {
try { KenjinxNative.setSurfaceRotationByAndroidRotation(rot) } catch (_: Throwable) {}
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val rot = this.display?.rotation
@@ -614,7 +612,7 @@ class MainActivity : BaseActivity() {
) { /* optional: Log/Toast */ }
private fun ensureNotificationPermission() {
if (android.os.Build.VERSION.SDK_INT >= 33) {
if (Build.VERSION.SDK_INT >= 33) {
val granted = ContextCompat.checkSelfPermission(
this, Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
@@ -674,6 +672,7 @@ class MainActivity : BaseActivity() {
} catch (_: Exception) { null }
}
@RequiresApi(Build.VERSION_CODES.R)
fun shutdownAndRestart() {
val packageManager = packageManager
val intent = packageManager.getLaunchIntentForPackage(packageName)
@@ -696,10 +695,10 @@ class MainActivity : BaseActivity() {
private fun setEmuRunningFlag(value: Boolean) {
try {
getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit()
.putBoolean(KEY_EMU_RUNNING, value)
.apply()
getSharedPreferences(PREFS, MODE_PRIVATE)
.edit {
putBoolean(KEY_EMU_RUNNING, value)
}
} catch (_: Throwable) { }
}
@@ -723,7 +722,7 @@ class MainActivity : BaseActivity() {
private fun coldResetIfZombie(phase: String) {
try {
val zombie = getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val zombie = getSharedPreferences(PREFS, MODE_PRIVATE)
.getBoolean(KEY_EMU_RUNNING, false)
if (zombie) {
clearEmuRunningFlag()
@@ -1,10 +1,13 @@
package org.kenjinx.android
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
import kotlin.math.abs
class PerformanceManager(private val activity: MainActivity) {
companion object {
@RequiresApi(Build.VERSION_CODES.R)
fun force60HzRefreshRate(enable: Boolean, activity: MainActivity) {
// Hack for MIUI devices since they don't support the standard Android APIs
try {
@@ -25,6 +28,7 @@ class PerformanceManager(private val activity: MainActivity) {
}
}
@RequiresApi(Build.VERSION_CODES.R)
fun setTurboMode(enable: Boolean) {
NativeHelpers.instance.setTurboMode(enable)
force60HzRefreshRate(enable, activity)
@@ -6,12 +6,12 @@ import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Base64
import android.widget.Toast
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
object ShortcutHelper {
@@ -33,7 +33,7 @@ object ShortcutHelper {
gridIconBitmap: Bitmap? = null,
gridIconBase64: String? = null
) {
val uri = runCatching { Uri.parse(bootPathUri) }.getOrNull()
val uri = runCatching { bootPathUri.toUri() }.getOrNull()
// --- Select icon (Grid bitmap > Base64 > App icon)
var icon = IconCompat.createWithResource(context, R.mipmap.ic_launcher)
@@ -1,5 +1,6 @@
package org.kenjinx.android
import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent
import android.content.BroadcastReceiver
@@ -16,9 +17,8 @@ import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.MotionEvent
import android.view.View
@SuppressLint("SourceLockedOrientationActivity")
object ShortcutUtils {
fun suggestLabelFromUri(uri: Uri): String {
@@ -47,7 +47,6 @@ object ShortcutUtils {
iconBitmap: Bitmap? = null,
onCompleted: (() -> Unit)? = null
): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) 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
@@ -118,7 +117,7 @@ object ShortcutUtils {
}
val filter = IntentFilter(ACTION_PIN_RESULT)
if (Build.VERSION.SDK_INT >= 33) {
activity.registerReceiver(pinResultReceiver!!, filter, Context.RECEIVER_NOT_EXPORTED)
activity.registerReceiver(pinResultReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
activity.registerReceiver(pinResultReceiver, filter)
@@ -1,5 +1,6 @@
package org.kenjinx.android
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
@@ -14,6 +15,7 @@ import android.util.Base64
import androidx.documentfile.provider.DocumentFile
import org.kenjinx.android.viewmodels.GameInfo
@SuppressLint("SourceLockedOrientationActivity")
class ShortcutWizardActivity : Activity() {
private val REQ_PICK_GAME = 1
@@ -100,7 +100,7 @@ class UiHandler {
if (inputText.value.isEmpty()) {
validation.value = "Must be between $minLength and $maxLength characters"
} else {
return inputText.value.length < minLength || inputText.value.length > maxLength
return inputText.value.length !in minLength..maxLength
}
return false
}
@@ -116,7 +116,7 @@ class UiHandler {
fun submit() {
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 "")
showMessageListener.value = false
@@ -30,7 +30,7 @@ private fun allCheatDirs(activity: Activity, titleId: String): List<File> {
private fun parseCheatNames(text: String): List<String> {
// Trim BOM, CRLF tolerant
val clean = text.replace("\uFEFF", "")
val rx = Regex("""(?m)^\s*\[(.+?)\]\s*$""")
val rx = Regex("""(?m)^\s*\[(.+?)]\s*$""")
return rx.findAll(clean)
.map { it.groupValues[1].trim() }
.filter { it.isNotEmpty() }
@@ -1,6 +1,7 @@
package org.kenjinx.android.cheats
import android.content.Context
import androidx.core.content.edit
class CheatPrefs(private val context: Context) {
private fun key(titleId: String) = "cheats_$titleId"
@@ -11,6 +12,6 @@ class CheatPrefs(private val context: Context) {
}
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.util.Log
import java.io.File
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
/* -------- Paths -------- */
@@ -246,7 +246,7 @@ fun importSaveFromZip(
}
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>/…
runCatching {
@@ -5,10 +5,8 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import org.kenjinx.android.MainActivity
@@ -138,10 +136,7 @@ class EmulationService : Service() {
private fun buildNotification(): Notification {
val openIntent = Intent(this, MainActivity::class.java)
val flags = if (Build.VERSION.SDK_INT >= 23)
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
else
PendingIntent.FLAG_UPDATE_CURRENT
val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
val pi = PendingIntent.getActivity(this, 0, openIntent, flags)
return NotificationCompat.Builder(this, CHANNEL_ID)
@@ -156,8 +151,7 @@ class EmulationService : Service() {
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= 26) {
val mgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val mgr = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val ch = NotificationChannel(
CHANNEL_ID,
"Emulation",
@@ -167,4 +161,3 @@ class EmulationService : Service() {
mgr.createNotificationChannel(ch)
}
}
}
@@ -212,11 +212,9 @@ class DlcViewModel(val titleId: String) {
}
var data: MutableList<DlcContainerList>? = null
private var jsonPath: String
private var jsonPath: String = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + "/dlc.json"
init {
jsonPath =
MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + "/dlc.json"
storageHelper = MainActivity.StorageHelper!!
reloadFromDisk()
@@ -1,5 +1,6 @@
package org.kenjinx.android.viewmodels
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
@@ -10,10 +11,15 @@ import org.kenjinx.android.KenjinxNative
class GameModel(var file: DocumentFile, val context: Context) {
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 fileName: String?
var fileBaseName: String?
var fileName = file.name
var fileBaseName: String? = fileName?.substringBeforeLast(".", fileName!!)
var fileSize = 0.0
var titleName: String? = null
var titleId: String? = null
@@ -21,17 +27,6 @@ class GameModel(var file: DocumentFile, val context: Context) {
var version: 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() {
val pid = open()
val gameInfo = GameInfo()
@@ -60,6 +55,7 @@ class GameModel(var file: DocumentFile, val context: Context) {
return descriptor?.fd ?: 0
}
@SuppressLint("UseKtx")
fun openUpdate(): Int {
if (titleId?.isNotEmpty() == true) {
val vm = TitleUpdateViewModel(titleId ?: "")
@@ -27,6 +27,8 @@ import org.kenjinx.android.UiHandler
import android.util.Log
import java.io.File
import java.util.TimeZone
import androidx.core.content.edit
import androidx.core.net.toUri
@SuppressLint("WrongConstant")
class MainViewModel(val activity: MainActivity) {
@@ -62,7 +64,7 @@ class MainViewModel(val activity: MainActivity) {
set(value) {
field = value
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
prefs.edit().putString("defaultGameFolderUri", value?.toString() ?: "").apply()
prefs.edit { putString("defaultGameFolderUri", value?.toString() ?: "") }
}
var gameHost: GameHost? = null
set(value) {
@@ -79,7 +81,7 @@ class MainViewModel(val activity: MainActivity) {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
val saved = prefs.getString("defaultGameFolderUri", "") ?: ""
if (saved.isNotEmpty()) {
defaultGameFolderUri = Uri.parse(saved)
defaultGameFolderUri = saved.toUri()
}
}
@@ -16,7 +16,7 @@ import java.io.File
class TitleUpdateViewModel(val titleId: String) {
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 storageHelper: SimpleStorageHelper
private var currentPaths: MutableList<String> = mutableListOf()
@@ -187,11 +187,9 @@ class TitleUpdateViewModel(val titleId: String) {
}
var data: TitleUpdateMetadata? = null
private var jsonPath: String
private var jsonPath: String = "${basePath}/${updateJsonName}"
init {
basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current)
jsonPath = "${basePath}/${updateJsonName}"
data = TitleUpdateMetadata()
if (File(jsonPath).exists()) {
@@ -53,7 +53,6 @@ import org.kenjinx.android.viewmodels.VSyncMode
import org.kenjinx.android.widgets.SimpleAlertDialog
import java.util.Locale
import kotlin.math.roundToInt
import android.net.Uri
import android.widget.Toast
import androidx.compose.foundation.layout.aspectRatio
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.ui.platform.LocalConfiguration
import org.kenjinx.android.viewmodels.QuickSettings.VirtualControllerPreset
import androidx.core.net.toUri
class GameViews {
@@ -338,7 +338,7 @@ class GameViews {
Toast.makeText(mainViewModel.activity, "Slot 1 is empty.", Toast.LENGTH_SHORT).show()
} else {
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()) {
val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size)
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()
} 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 = {
val qs = QuickSettings(mainViewModel.activity)
@@ -356,7 +356,7 @@ class GameViews {
Toast.makeText(mainViewModel.activity, "Slot 2 is empty.", Toast.LENGTH_SHORT).show()
} else {
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()) {
val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size)
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()
} 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 = {
val qs = QuickSettings(mainViewModel.activity)
@@ -374,7 +374,7 @@ class GameViews {
Toast.makeText(mainViewModel.activity, "Slot 3 is empty.", Toast.LENGTH_SHORT).show()
} else {
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()) {
val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size)
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()
} 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)) {
@@ -394,7 +394,7 @@ class GameViews {
Toast.makeText(mainViewModel.activity, "Slot 4 is empty.", Toast.LENGTH_SHORT).show()
} else {
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()) {
val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size)
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()
} 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 = {
val qs = QuickSettings(mainViewModel.activity)
@@ -412,7 +412,7 @@ class GameViews {
Toast.makeText(mainViewModel.activity, "Slot 5 is empty.", Toast.LENGTH_SHORT).show()
} else {
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()) {
val ok = KenjinxNative.amiiboLoadBin(bytes, bytes.size)
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()
} 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 = {
KenjinxNative.amiiboClear()
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.widgets.SimpleAlertDialog
import org.kenjinx.android.ShortcutUtils
import org.kenjinx.android.ShortcutWizardActivity
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.TextButton
import androidx.compose.runtime.mutableFloatStateOf
import androidx.documentfile.provider.DocumentFile
// Cheats
@@ -162,19 +162,19 @@ class HomeViews {
var isFabVisible by remember { mutableStateOf(true) }
val isNavigating = remember { mutableStateOf(false) }
// NEW: Amiibo slot picker state
// Amiibo slot picker state
val showAmiiboSlotDialog = remember { mutableStateOf(false) }
val pendingSlot = remember { mutableStateOf(1) }
// NEW: Cheats UI state
// Cheats UI state
val openCheatsDialog = remember { mutableStateOf(false) }
val cheatsForSelected = remember { mutableStateOf(listOf<CheatItem>()) }
val enabledCheatKeys = remember { mutableStateOf(mutableSetOf<String>()) }
// NEW: Mods UI state
// Mods UI state
val openModsDialog = remember { mutableStateOf(false) }
val modsForSelected = remember { mutableStateOf(listOf<String>()) }
val modsImportProgress = remember { mutableStateOf(0f) }
val modsImportProgress = remember { mutableFloatStateOf(0f) }
val modsImportBusy = remember { mutableStateOf(false) }
val modsImportStatusText = remember { mutableStateOf("") }
@@ -182,8 +182,8 @@ class HomeViews {
val openSavesDialog = remember { mutableStateOf(false) }
val saveImportBusy = remember { mutableStateOf(false) }
val saveExportBusy = remember { mutableStateOf(false) }
val saveImportProgress = remember { mutableStateOf(0f) }
val saveExportProgress = remember { mutableStateOf(0f) }
val saveImportProgress = remember { mutableFloatStateOf(0f) }
val saveExportProgress = remember { mutableFloatStateOf(0f) }
val saveImportStatus = remember { mutableStateOf("") }
val saveExportStatus = remember { mutableStateOf("") }
@@ -198,13 +198,13 @@ class HomeViews {
val tIdNow = viewModel.mainViewModel?.selected?.titleId.orEmpty()
if (uri != null && act != null && tIdNow.isNotEmpty()) {
saveImportBusy.value = true
saveImportProgress.value = 0f
saveImportProgress.floatValue = 0f
saveImportStatus.value = "Starting…"
thread {
val res = importSaveFromZip(act, uri) { prog ->
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}"
}
saveImportBusy.value = false
@@ -223,13 +223,13 @@ class HomeViews {
val tIdNow = viewModel.mainViewModel?.selected?.titleId.orEmpty()
if (uri != null && act != null && tIdNow.isNotEmpty()) {
saveExportBusy.value = true
saveExportProgress.value = 0f
saveExportProgress.floatValue = 0f
saveExportStatus.value = "Starting…"
thread {
val res = exportSaveToZip(act, tIdNow, uri) { prog ->
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}"
}
saveExportBusy.value = false
@@ -250,7 +250,7 @@ class HomeViews {
val context = LocalContext.current
// NEW: Amiibo Launcher (OpenDocument)
// Amiibo Launcher (OpenDocument)
val pickAmiiboLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
@@ -275,7 +275,7 @@ class HomeViews {
}
}
// NEW: Cheats Import (.txt)
// Cheats Import (.txt)
val importCheatLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri: Uri? ->
@@ -316,7 +316,7 @@ class HomeViews {
} catch (_: Exception) {}
modsImportBusy.value = true
modsImportProgress.value = 0f
modsImportProgress.floatValue = 0f
modsImportStatusText.value = "Starting…"
thread {
@@ -325,7 +325,7 @@ class HomeViews {
titleId,
uri
) { prog ->
modsImportProgress.value = prog.fraction
modsImportProgress.floatValue = prog.fraction
modsImportStatusText.value = if (prog.currentEntry.isNotEmpty())
"Copying: ${prog.currentEntry}"
else
@@ -642,7 +642,7 @@ class HomeViews {
// Amiibo slot chooser dialog (outside of game)
if (showAmiiboSlotDialog.value) {
androidx.compose.material3.AlertDialog(
AlertDialog(
onDismissRequest = { showAmiiboSlotDialog.value = false },
title = { Text("Import Amiibo") },
text = {
@@ -732,7 +732,7 @@ class HomeViews {
if (viewModel.mainViewModel?.selected != null) {
// Press the cheat buttons before starting (Run button).
val gmSel = viewModel.mainViewModel!!.selected!!
val gmSel = viewModel.mainViewModel.selected!!
val tId = gmSel.titleId ?: ""
val act = viewModel.activity
@@ -1125,7 +1125,7 @@ class HomeViews {
.padding(top = 12.dp),
horizontalArrangement = Arrangement.End
) {
androidx.compose.material3.TextButton(
TextButton(
onClick = { openSavesDialog.value = false }
) { Text("Close") }
}