diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt index 3be7c2da1..a277224ca 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt @@ -1,6 +1,7 @@ package com.topjohnwu.magisk.ui import android.Manifest +import android.Manifest.permission.REQUEST_INSTALL_PACKAGES import android.annotation.SuppressLint import android.content.Intent import android.content.pm.ApplicationInfo @@ -8,35 +9,45 @@ import android.os.Bundle import android.view.MenuItem import android.view.View import android.view.WindowManager +import android.widget.Toast import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.view.forEach import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDirections import com.topjohnwu.magisk.MainDirections import com.topjohnwu.magisk.R import com.topjohnwu.magisk.arch.BaseViewModel +import com.topjohnwu.magisk.arch.NavigationActivity import com.topjohnwu.magisk.arch.startAnimations import com.topjohnwu.magisk.arch.viewModel import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Const 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.ktx.toast 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.ui.home.HomeFragmentDirections +import com.topjohnwu.magisk.ui.theme.Theme import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.Shortcuts +import kotlinx.coroutines.launch import java.io.File import com.topjohnwu.magisk.core.R as CoreR class MainViewModel : BaseViewModel() -class MainActivity : SplashActivity() { +class MainActivity : NavigationActivity(), SplashScreenHost { override val layoutRes = R.layout.activity_main_md2 override val viewModel by viewModel() override val navHostId: Int = R.id.main_nav_host + override val splashController = SplashController(this) override val snackbarView: View get() { val fragmentOverride = currentFragment?.snackbarView @@ -54,8 +65,20 @@ class MainActivity : SplashActivity() { 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") - override fun showMainUI(savedInstanceState: Bundle?) { + override fun onCreateUi(savedInstanceState: Bundle?) { setContentView() showUnsupportedMessage() askForHomeShortcut() @@ -165,6 +188,31 @@ class MainActivity : SplashActivity() { } } + @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() { if (Info.env.isUnsupported) { MagiskDialog(this).apply { diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt deleted file mode 100644 index 2e1906486..000000000 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt +++ /dev/null @@ -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 : NavigationActivity() { - - 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) - } - } -} diff --git a/app/core/build.gradle.kts b/app/core/build.gradle.kts index 2f7832ce9..8dae29848 100644 --- a/app/core/build.gradle.kts +++ b/app/core/build.gradle.kts @@ -63,7 +63,7 @@ dependencies { implementation("androidx.room:room-ktx:${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.activity:activity:1.9.0") implementation("androidx.collection:collection-ktx:1.4.1") diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/AppContext.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/AppContext.kt index 0965c2540..c20055ba9 100644 --- a/app/core/src/main/java/com/topjohnwu/magisk/core/AppContext.kt +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/AppContext.kt @@ -1,6 +1,5 @@ package com.topjohnwu.magisk.core -import android.Manifest.permission.REQUEST_INSTALL_PACKAGES import android.app.Activity import android.app.Application import android.app.LocaleManager @@ -12,24 +11,15 @@ import android.os.Build import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.system.Os -import androidx.activity.ComponentActivity -import androidx.lifecycle.lifecycleScope import androidx.profileinstaller.ProfileInstaller 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.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.NetworkObserver import com.topjohnwu.magisk.core.utils.ProcessLifecycle import com.topjohnwu.magisk.core.utils.RootUtils import com.topjohnwu.magisk.core.utils.ShellInit import com.topjohnwu.magisk.view.Notifications -import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.ipc.RootService @@ -38,8 +28,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.asExecutor import kotlinx.coroutines.launch import timber.log.Timber -import java.io.File -import java.io.IOException import java.lang.ref.WeakReference import kotlin.system.exitProcess @@ -141,71 +129,3 @@ object AppContext : ContextWrapper(null), override fun onLowMemory() {} override fun onTrimMemory(level: Int) {} } - -fun 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) -} diff --git a/app/core/src/main/java/com/topjohnwu/magisk/core/base/SplashScreen.kt b/app/core/src/main/java/com/topjohnwu/magisk/core/base/SplashScreen.kt new file mode 100644 index 000000000..338509797 --- /dev/null +++ b/app/core/src/main/java/com/topjohnwu/magisk/core/base/SplashScreen.kt @@ -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(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() + } +}