Files
rl-mobile/Manager/app/src/main/kotlin/com/meowarex/rlmobile/MainActivity.kt
T
2026-05-24 21:18:53 +10:00

199 lines
8.3 KiB
Kotlin

package com.meowarex.rlmobile
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.animation.core.*
import androidx.compose.runtime.*
import androidx.compose.ui.unit.IntOffset
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.compose.LifecycleResumeEffect
import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.*
import cafe.adriel.voyager.transitions.SlideTransition
import com.meowarex.rlmobile.MainActivity.Companion.EXTRA_COMPONENT_TYPE
import com.meowarex.rlmobile.MainActivity.Companion.EXTRA_FILE_PATH
import com.meowarex.rlmobile.MainActivity.Companion.EXTRA_PACKAGE_NAME
import com.meowarex.rlmobile.manager.*
import com.meowarex.rlmobile.patcher.InstallMetadata
import com.meowarex.rlmobile.ui.screens.home.HomeScreen
import com.meowarex.rlmobile.ui.screens.patching.PatchingScreen
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
import com.meowarex.rlmobile.ui.screens.permissions.PermissionsModel
import com.meowarex.rlmobile.ui.screens.permissions.PermissionsScreen
import com.meowarex.rlmobile.ui.theme.ManagerTheme
import com.meowarex.rlmobile.ui.widgets.updater.UpdaterDialog
import com.meowarex.rlmobile.util.*
import com.github.diamondminer88.zip.ZipReader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.io.File
class MainActivity : ComponentActivity() {
private val permissions: PermissionsModel by viewModel()
private val preferences: PreferencesManager by inject()
private val overlays: OverlayManager by inject()
private val paths: PathManager by inject()
private val json: Json by inject()
private val scope = CoroutineScope(Dispatchers.Default)
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
installSplashScreen()
super.onCreate(savedInstanceState)
setContent {
// Refresh permissions when the activity resumes
LifecycleResumeEffect(Unit) {
permissions.refresh()
onPauseOrDispose {}
}
ManagerTheme(
theme = preferences.theme,
dynamicColor = preferences.dynamicColor,
) {
if (BuildConfig.RELEASE) {
UpdaterDialog()
}
@OptIn(ExperimentalVoyagerApi::class)
CompositionLocalProvider(
LocalNavigatorSaver provides parcelableNavigatorSaver(),
) {
Navigator(
screen = HomeScreen(),
onBackPressed = null,
) { navigator ->
// Open the permissions screen whenever permissions are insufficient
LaunchedEffect(permissions.requiredPermsGranted) {
if (!permissions.requiredPermsGranted)
navigator.pushOnce(PermissionsScreen())
}
DisposableEffect(Unit) {
this@MainActivity.intent?.let { handleNewIntent(it, navigator) }
fun handle(intent: Intent) = handleNewIntent(intent, navigator)
addOnNewIntentListener(::handle)
onDispose { removeOnNewIntentListener(::handle) }
}
BackHandler {
navigator.back(this@MainActivity)
}
SlideTransition(
navigator = navigator,
disposeScreenAfterTransitionEnd = true,
animationSpec = spring(
stiffness = Spring.StiffnessMedium,
visibilityThreshold = IntOffset.VisibilityThreshold,
)
)
}
overlays.Overlays()
}
}
}
}
private fun handleNewIntent(intent: Intent, navigator: Navigator) = scope.launchBlock {
when (intent.action) {
INTENT_REINSTALL -> {
val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) ?: run {
Log.w(BuildConfig.TAG, "Missing $EXTRA_PACKAGE_NAME extra for intent $INTENT_REINSTALL")
return@launchBlock
}
navigator.push(handleReinstall(packageName))
}
INTENT_IMPORT_COMPONENT -> {
val path = intent.getStringExtra(EXTRA_FILE_PATH) ?: run {
Log.w(BuildConfig.TAG, "Missing $EXTRA_FILE_PATH extra for intent $INTENT_IMPORT_COMPONENT")
mainThread { showToast(R.string.intent_import_component_failure) }
return@launchBlock
}
val componentType = intent.getStringExtra(EXTRA_COMPONENT_TYPE) ?: run {
Log.w(BuildConfig.TAG, "Missing $EXTRA_COMPONENT_TYPE extra for intent $INTENT_IMPORT_COMPONENT")
mainThread { showToast(R.string.intent_import_component_failure) }
return@launchBlock
}
val file = File("/data/local/tmp", path)
if (!file.exists()) {
Log.w(BuildConfig.TAG, "Intent $INTENT_IMPORT_COMPONENT specified an invalid file!")
mainThread { showToast(R.string.intent_import_component_failure) }
return@launchBlock
}
val targetDir = when (componentType) {
"tidal" -> paths.customTidalApksDir
"patches" -> paths.customPatchesDir
else -> {
Log.w(BuildConfig.TAG, "Extra $EXTRA_COMPONENT_TYPE is not a valid value!")
mainThread { showToast(R.string.intent_import_component_failure) }
return@launchBlock
}
}
try {
file.copyTo(targetDir.resolve(file.name), overwrite = true)
file.delete() // This most likely silently fails
} catch (e: Exception) {
Log.e(BuildConfig.TAG, "Failed to import custom component", e)
mainThread { showToast(R.string.intent_import_component_failure) }
}
mainThread { showToast(R.string.intent_import_component_success, file.name) }
}
else -> {
Log.w(BuildConfig.TAG, "Unhandled intent ${intent.action}")
}
}
}
private suspend fun handleReinstall(packageName: String): Screen {
val metadata = try {
val applicationInfo = packageManager.getApplicationInfo(packageName, 0)
val metadataFile = ZipReader(applicationInfo.publicSourceDir)
.use { it.openEntry("rlmobile.json")?.read() }
metadataFile?.let { json.decodeFromStream<InstallMetadata>(it.inputStream()) }
} catch (t: Throwable) {
Log.w(BuildConfig.TAG, "Failed to parse Radiant Lyrics install metadata from package $packageName", t)
mainThread { showToast(R.string.intent_reinstall_fail) }
null
}
val patchOptions = metadata?.options
?: PatchOptions.Default.copy(packageName = packageName)
return PatchingScreen(patchOptions)
}
companion object {
const val INTENT_REINSTALL = "com.meowarex.rlmobile.REINSTALL"
const val INTENT_IMPORT_COMPONENT = "com.meowarex.rlmobile.IMPORT_COMPONENT"
const val EXTRA_PACKAGE_NAME = "rlmobile.packageName"
const val EXTRA_FILE_PATH = "rlmobile.file"
const val EXTRA_COMPONENT_TYPE = "rlmobile.componentType"
}
}