Move SplashActivity logic into core module

This commit is contained in:
topjohnwu 2024-07-11 22:08:01 -07:00
parent fcb7ebb090
commit ddae568741
5 changed files with 210 additions and 193 deletions

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.ui package com.topjohnwu.magisk.ui
import android.Manifest import android.Manifest
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
@ -8,35 +9,45 @@ import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.Toast
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import com.topjohnwu.magisk.MainDirections import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.startAnimations import com.topjohnwu.magisk.arch.startAnimations
import com.topjohnwu.magisk.arch.viewModel import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.SplashController
import com.topjohnwu.magisk.core.base.SplashScreenHost
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.model.module.LocalModule import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import com.topjohnwu.magisk.core.R as CoreR import com.topjohnwu.magisk.core.R as CoreR
class MainViewModel : BaseViewModel() class MainViewModel : BaseViewModel()
class MainActivity : SplashActivity<ActivityMainMd2Binding>() { class MainActivity : NavigationActivity<ActivityMainMd2Binding>(), SplashScreenHost {
override val layoutRes = R.layout.activity_main_md2 override val layoutRes = R.layout.activity_main_md2
override val viewModel by viewModel<MainViewModel>() override val viewModel by viewModel<MainViewModel>()
override val navHostId: Int = R.id.main_nav_host override val navHostId: Int = R.id.main_nav_host
override val splashController = SplashController(this)
override val snackbarView: View override val snackbarView: View
get() { get() {
val fragmentOverride = currentFragment?.snackbarView val fragmentOverride = currentFragment?.snackbarView
@ -54,8 +65,20 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
private var isRootFragment = true private var isRootFragment = true
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(Theme.selected.themeRes)
splashController.preOnCreate()
super.onCreate(savedInstanceState)
splashController.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
splashController.onResume()
}
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
override fun showMainUI(savedInstanceState: Bundle?) { override fun onCreateUi(savedInstanceState: Bundle?) {
setContentView() setContentView()
showUnsupportedMessage() showUnsupportedMessage()
askForHomeShortcut() askForHomeShortcut()
@ -165,6 +188,31 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
} }
} }
@SuppressLint("InlinedApi")
override fun showInvalidStateMessage(): Unit = runOnUiThread {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_nonroot_stub_title)
setMessage(CoreR.string.unsupport_nonroot_stub_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = CoreR.string.install
onClick {
withPermission(REQUEST_INSTALL_PACKAGES) {
if (!it) {
toast(CoreR.string.install_unknown_denied, Toast.LENGTH_SHORT)
showInvalidStateMessage()
} else {
lifecycleScope.launch {
AppMigration.restore(this@MainActivity)
}
}
}
}
}
setCancelable(false)
show()
}
}
private fun showUnsupportedMessage() { private fun showUnsupportedMessage() {
if (Info.env.isUnsupported) { if (Info.env.isUnsupported) {
MagiskDialog(this).apply { MagiskDialog(this).apply {

View File

@ -1,110 +0,0 @@
package com.topjohnwu.magisk.ui
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.annotation.SuppressLint
import android.os.Bundle
import android.widget.Toast
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.base.relaunch
import com.topjohnwu.magisk.core.initializeOnSplashScreen
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch
@SuppressLint("CustomSplashScreen")
abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
companion object {
private var splashShown = false
}
private var needShowMainUI = false
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(Theme.selected.themeRes)
if (isRunningAsStub && !splashShown) {
// Manually apply splash theme for stub
theme.applyStyle(R.style.StubSplashTheme, true)
}
super.onCreate(savedInstanceState)
if (!isRunningAsStub) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !splashShown }
}
if (splashShown) {
doShowMainUI(savedInstanceState)
} else {
Shell.getShell(Shell.EXECUTOR) {
if (isRunningAsStub && !it.isRoot) {
showInvalidStateMessage()
return@getShell
}
initializeOnSplashScreen {
splashShown = true
if (isRunningAsStub) {
// Re-launch main activity without splash theme
relaunch()
} else {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
doShowMainUI(savedInstanceState)
} else {
needShowMainUI = true
}
}
}
}
}
}
private fun doShowMainUI(savedInstanceState: Bundle?) {
needShowMainUI = false
showMainUI(savedInstanceState)
}
abstract fun showMainUI(savedInstanceState: Bundle?)
@SuppressLint("InlinedApi")
private fun showInvalidStateMessage(): Unit = runOnUiThread {
MagiskDialog(this).apply {
setTitle(R.string.unsupport_nonroot_stub_title)
setMessage(R.string.unsupport_nonroot_stub_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
onClick {
withPermission(REQUEST_INSTALL_PACKAGES) {
if (!it) {
toast(R.string.install_unknown_denied, Toast.LENGTH_SHORT)
showInvalidStateMessage()
} else {
lifecycleScope.launch {
AppMigration.restore(this@SplashActivity)
}
}
}
}
}
setCancelable(false)
show()
}
}
override fun onResume() {
super.onResume()
if (needShowMainUI) {
doShowMainUI(null)
}
}
}

View File

@ -63,7 +63,7 @@ dependencies {
implementation("androidx.room:room-ktx:${vRoom}") implementation("androidx.room:room-ktx:${vRoom}")
ksp("androidx.room:room-compiler:${vRoom}") ksp("androidx.room:room-compiler:${vRoom}")
api("androidx.core:core-splashscreen:1.0.1") implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.activity:activity:1.9.0") implementation("androidx.activity:activity:1.9.0")
implementation("androidx.collection:collection-ktx:1.4.1") implementation("androidx.collection:collection-ktx:1.4.1")

View File

@ -1,6 +1,5 @@
package com.topjohnwu.magisk.core package com.topjohnwu.magisk.core
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.app.LocaleManager import android.app.LocaleManager
@ -12,24 +11,15 @@ import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.os.Bundle import android.os.Bundle
import android.system.Os import android.system.Os
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import androidx.profileinstaller.ProfileInstaller import androidx.profileinstaller.ProfileInstaller
import com.topjohnwu.magisk.StubApk import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME
import com.topjohnwu.magisk.core.base.IActivityExtension
import com.topjohnwu.magisk.core.base.UntrackedActivity import com.topjohnwu.magisk.core.base.UntrackedActivity
import com.topjohnwu.magisk.core.base.launchPackage
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.utils.LocaleSetting import com.topjohnwu.magisk.core.utils.LocaleSetting
import com.topjohnwu.magisk.core.utils.NetworkObserver import com.topjohnwu.magisk.core.utils.NetworkObserver
import com.topjohnwu.magisk.core.utils.ProcessLifecycle import com.topjohnwu.magisk.core.utils.ProcessLifecycle
import com.topjohnwu.magisk.core.utils.RootUtils import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.core.utils.ShellInit import com.topjohnwu.magisk.core.utils.ShellInit
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.superuser.ipc.RootService import com.topjohnwu.superuser.ipc.RootService
@ -38,8 +28,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.asExecutor import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.io.IOException
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -141,71 +129,3 @@ object AppContext : ContextWrapper(null),
override fun onLowMemory() {} override fun onLowMemory() {}
override fun onTrimMemory(level: Int) {} override fun onTrimMemory(level: Int) {}
} }
fun <T> T.initializeOnSplashScreen(launchUi: Runnable)
where T : ComponentActivity, T : IActivityExtension {
val prevPkg = launchPackage
val prevConfig = intent.getBundleExtra(Const.Key.PREV_CONFIG)
val isPackageMigration = prevPkg != null && prevConfig != null
Config.init(prevConfig)
if (packageName != APP_PACKAGE_NAME) {
runCatching {
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
packageManager.getApplicationInfo(APP_PACKAGE_NAME, 0)
Shell.cmd("(pm uninstall $APP_PACKAGE_NAME)& >/dev/null 2>&1").exec()
}
} else {
if (Config.suManager.isNotEmpty()) {
Config.suManager = ""
}
if (isPackageMigration) {
Shell.cmd("(pm uninstall $prevPkg)& >/dev/null 2>&1").exec()
}
}
if (isPackageMigration) {
runOnUiThread {
// Relaunch the process after package migration
StubApk.restartProcess(this)
}
return
}
// Validate stub APK
if (isRunningAsStub && (
// Version mismatch
Info.stub!!.version != BuildConfig.STUB_VERSION ||
// Not properly patched
intent.component!!.className.contains(AppMigration.PLACEHOLDER))
) {
withPermission(REQUEST_INSTALL_PACKAGES) { granted ->
if (granted) {
lifecycleScope.launch {
val apk = File(cacheDir, "stub.apk")
try {
assets.open("stub.apk").writeTo(apk)
AppMigration.upgradeStub(this@initializeOnSplashScreen, apk)?.let {
startActivity(it)
}
} catch (e: IOException) {
Timber.e(e)
}
}
}
}
return
}
JobService.schedule(this)
Shortcuts.setupDynamic(this)
// Pre-fetch network services
ServiceLocator.networkService
// Wait for root service
RootUtils.Connection.await()
runOnUiThread(launchUi)
}

View File

@ -0,0 +1,159 @@
package com.topjohnwu.magisk.core.base
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.StubApk
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.JobService
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.io.IOException
interface SplashScreenHost : IActivityExtension {
val splashController: SplashController<*>
fun onCreateUi(savedInstanceState: Bundle?)
fun showInvalidStateMessage()
}
class SplashController<T>(private val activity: T)
where T : ComponentActivity, T: SplashScreenHost {
companion object {
private var splashShown = false
}
private var shouldCreateUiOnResume = false
fun preOnCreate() {
if (isRunningAsStub && !splashShown) {
// Manually apply splash theme for stub
activity.theme.applyStyle(R.style.StubSplashTheme, true)
}
}
fun onCreate(savedInstanceState: Bundle?) {
if (!isRunningAsStub) {
val splashScreen = activity.installSplashScreen()
splashScreen.setKeepOnScreenCondition { !splashShown }
}
if (splashShown) {
doCreateUi(savedInstanceState)
} else {
Shell.getShell(Shell.EXECUTOR) {
if (isRunningAsStub && !it.isRoot) {
activity.showInvalidStateMessage()
return@getShell
}
activity.initializeApp()
activity.runOnUiThread {
splashShown = true
if (isRunningAsStub) {
// Re-launch main activity without splash theme
activity.relaunch()
} else {
if (activity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
doCreateUi(savedInstanceState)
} else {
shouldCreateUiOnResume = true
}
}
}
}
}
}
fun onResume() {
if (shouldCreateUiOnResume) {
doCreateUi(null)
}
}
private fun doCreateUi(savedInstanceState: Bundle?) {
shouldCreateUiOnResume = false
activity.onCreateUi(savedInstanceState)
}
private fun T.initializeApp() {
val prevPkg = launchPackage
val prevConfig = intent.getBundleExtra(Const.Key.PREV_CONFIG)
val isPackageMigration = prevPkg != null && prevConfig != null
Config.init(prevConfig)
if (packageName != APP_PACKAGE_NAME) {
runCatching {
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
packageManager.getApplicationInfo(APP_PACKAGE_NAME, 0)
Shell.cmd("(pm uninstall $APP_PACKAGE_NAME)& >/dev/null 2>&1").exec()
}
} else {
if (Config.suManager.isNotEmpty()) {
Config.suManager = ""
}
if (isPackageMigration) {
Shell.cmd("(pm uninstall $prevPkg)& >/dev/null 2>&1").exec()
}
}
if (isPackageMigration) {
runOnUiThread {
// Relaunch the process after package migration
StubApk.restartProcess(this)
}
return
}
// Validate stub APK
if (isRunningAsStub && (
// Version mismatch
Info.stub!!.version != BuildConfig.STUB_VERSION ||
// Not properly patched
intent.component!!.className.contains(AppMigration.PLACEHOLDER))
) {
withPermission(REQUEST_INSTALL_PACKAGES) { granted ->
if (granted) {
lifecycleScope.launch {
val apk = File(cacheDir, "stub.apk")
try {
assets.open("stub.apk").writeTo(apk)
AppMigration.upgradeStub(activity, apk)?.let {
startActivity(it)
}
} catch (e: IOException) {
Timber.e(e)
}
}
}
}
return
}
JobService.schedule(this)
Shortcuts.setupDynamic(this)
// Pre-fetch network services
ServiceLocator.networkService
// Wait for root service
RootUtils.Connection.await()
}
}