Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
package com.topjohnwu.magisk.arch

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.ui.deny.DenyListViewModel
import com.topjohnwu.magisk.ui.flash.FlashViewModel
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.install.InstallViewModel
import com.topjohnwu.magisk.ui.log.LogViewModel
import com.topjohnwu.magisk.ui.module.ActionViewModel
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel

object VMFactory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when (modelClass) {
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
InstallViewModel::class.java ->
InstallViewModel(ServiceLocator.networkService)
SuRequestViewModel::class.java ->
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
else -> modelClass.newInstance()
} as T
}
val VMFactory: ViewModelProvider.Factory = viewModelFactory {
initializer { HomeViewModel(ServiceLocator.networkService) }
initializer { LogViewModel(ServiceLocator.logRepo) }
initializer { SuperuserViewModel(ServiceLocator.policyDB) }
initializer { InstallViewModel(ServiceLocator.networkService) }
initializer { SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs) }
initializer { DenyListViewModel() }
initializer { FlashViewModel() }
initializer { ActionViewModel() }
initializer { ModuleViewModel() }
initializer { SettingsViewModel() }
}
120 changes: 78 additions & 42 deletions app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.net.Uri
import android.os.Bundle
import androidx.core.net.toUri
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.ComponentActivity
Expand All @@ -18,11 +16,15 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalResources
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
Expand All @@ -41,6 +43,7 @@ import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.ui.component.rememberConfirmDialog
import com.topjohnwu.magisk.ui.deny.DenyListScreen
import com.topjohnwu.magisk.ui.deny.DenyListViewModel
import com.topjohnwu.magisk.ui.flash.FlashScreen
Expand All @@ -57,6 +60,7 @@ import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.view.Shortcuts
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.io.File
import com.topjohnwu.magisk.core.R as CoreR

class MainActivity : ComponentActivity(), SplashScreenHost {
Expand Down Expand Up @@ -122,7 +126,16 @@ class MainActivity : ComponentActivity(), SplashScreenHost {
),
entryProvider = entryProvider {
entry<Route.Main> {
MainScreen(initialTab = initialTab)
val superuserVm: SuperuserViewModel = viewModel(
viewModelStoreOwner = this@MainActivity, factory = VMFactory
)
MainScreen(
initialTab = initialTab,
superuserViewModel = superuserVm,
onAuthenticate = { action ->
extension.withAuthentication { if (it) action() }
}
)
}
entry<Route.DenyList> { _ ->
val vm: DenyListViewModel = viewModel(factory = VMFactory)
Expand All @@ -149,7 +162,14 @@ class MainActivity : ComponentActivity(), SplashScreenHost {
extension.withAuthentication { if (it) onSuccess() }
}
}
SuperuserDetailScreen(uid = key.uid, viewModel = vm, onBack = { navigator.pop() })
SuperuserDetailScreen(
uid = key.uid,
viewModel = vm,
onBack = { navigator.pop() },
onAuthenticate = { action ->
extension.withAuthentication { if (it) action() }
}
)
}
entry<Route.Action> { key ->
val vm: ActionViewModel = viewModel(factory = VMFactory)
Expand All @@ -165,15 +185,30 @@ class MainActivity : ComponentActivity(), SplashScreenHost {
}
)
}
MainActivityDialogs(activity = this@MainActivity)
MainActivityDialogs(
showInvalid = showInvalidState.collectAsStateWithLifecycle().value,
unsupportedMessages = showUnsupported.collectAsStateWithLifecycle().value,
showShortcut = showShortcutPrompt.collectAsStateWithLifecycle().value,
onInvalidConfirmed = {
showInvalidState.value = false
handleInvalidStateInstall()
},
onShortcutConfirmed = {
showShortcutPrompt.value = false
Shortcuts.addHomeIcon(this@MainActivity)
},
onShortcutDismissed = {
showShortcutPrompt.value = false
}
)
}
}
}
}

@Composable
private fun HandleFlashIntent(navigator: Navigator) {
val intentVersion by intentState.collectAsState()
val intentVersion by intentState.collectAsStateWithLifecycle()
LaunchedEffect(intentVersion) {
val currentIntent = intent ?: return@LaunchedEffect
if (currentIntent.action == FlashUtils.INTENT_FLASH) {
Expand Down Expand Up @@ -234,8 +269,8 @@ class MainActivity : ComponentActivity(), SplashScreenHost {
}
if (!Info.isEmulator && Info.env.isActive && System.getenv("PATH")
?.split(':')
?.filterNot { java.io.File("$it/magisk").exists() }
?.any { java.io.File("$it/su").exists() } == true) {
?.filterNot { File("$it/magisk").exists() }
?.any { File("$it/su").exists() } == true) {
messages.add(CoreR.string.unsupport_general_title to CoreR.string.unsupport_other_su_msg)
}
if (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
Expand All @@ -260,57 +295,58 @@ class MainActivity : ComponentActivity(), SplashScreenHost {
}

@Composable
private fun MainActivityDialogs(activity: MainActivity) {
val showInvalid by activity.showInvalidState.collectAsState()
val unsupportedMessages by activity.showUnsupported.collectAsState()
val showShortcut by activity.showShortcutPrompt.collectAsState()

val invalidDialog = com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
onConfirm = {
activity.showInvalidState.value = false
activity.handleInvalidStateInstall()
},
private fun MainActivityDialogs(
showInvalid: Boolean,
unsupportedMessages: List<Pair<Int, Int>>,
showShortcut: Boolean,
onInvalidConfirmed: () -> Unit,
onShortcutConfirmed: () -> Unit,
onShortcutDismissed: () -> Unit,
modifier: Modifier = Modifier
) {
val resources = LocalResources.current
val invalidDialog = rememberConfirmDialog(
onConfirm = onInvalidConfirmed,
onDismiss = {}
)

LaunchedEffect(showInvalid) {
if (showInvalid) {
invalidDialog.showConfirm(
title = activity.getString(CoreR.string.unsupport_nonroot_stub_title),
content = activity.getString(CoreR.string.unsupport_nonroot_stub_msg),
confirm = activity.getString(CoreR.string.install),
title = resources.getString(CoreR.string.unsupport_nonroot_stub_title),
content = resources.getString(CoreR.string.unsupport_nonroot_stub_msg),
confirm = resources.getString(CoreR.string.install),
)
}
}

for ((index, pair) in unsupportedMessages.withIndex()) {
val (titleRes, msgRes) = pair
val show = rememberSaveable { androidx.compose.runtime.mutableStateOf(true) }
com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
onConfirm = { show.value = false },
).also { dialog ->
LaunchedEffect(Unit) {
dialog.showConfirm(
title = activity.getString(titleRes),
content = activity.getString(msgRes),
)
}
var currentUnsupportedIndex by rememberSaveable { mutableIntStateOf(0) }
val unsupportedDialog = rememberConfirmDialog(
onConfirm = { currentUnsupportedIndex++ },
onDismiss = { currentUnsupportedIndex++ }
)

val currentUnsupported = unsupportedMessages.getOrNull(currentUnsupportedIndex)
LaunchedEffect(currentUnsupported) {
if (currentUnsupported != null) {
val (titleRes, msgRes) = currentUnsupported
unsupportedDialog.showConfirm(
title = resources.getString(titleRes),
content = resources.getString(msgRes),
)
}
}

val shortcutDialog = com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
onConfirm = {
activity.showShortcutPrompt.value = false
Shortcuts.addHomeIcon(activity)
},
onDismiss = { activity.showShortcutPrompt.value = false }
val shortcutDialog = rememberConfirmDialog(
onConfirm = onShortcutConfirmed,
onDismiss = onShortcutDismissed
)

LaunchedEffect(showShortcut) {
if (showShortcut) {
shortcutDialog.showConfirm(
title = activity.getString(CoreR.string.add_shortcut_title),
content = activity.getString(CoreR.string.add_shortcut_msg),
title = resources.getString(CoreR.string.add_shortcut_title),
content = resources.getString(CoreR.string.add_shortcut_msg),
)
}
}
Expand Down
49 changes: 34 additions & 15 deletions app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
Expand Down Expand Up @@ -78,7 +79,12 @@ enum class Tab(val titleRes: Int, val iconRes: Int) {
}

@Composable
fun MainScreen(initialTab: Int = Tab.HOME.ordinal) {
fun MainScreen(
modifier: Modifier = Modifier,
initialTab: Int = Tab.HOME.ordinal,
superuserViewModel: SuperuserViewModel? = null,
onAuthenticate: ((onSuccess: () -> Unit) -> Unit)? = null,
) {
val navigator = LocalNavigator.current
val visibleTabs = remember {
Tab.entries.filter { tab ->
Expand All @@ -92,50 +98,63 @@ fun MainScreen(initialTab: Int = Tab.HOME.ordinal) {
val initialPage = visibleTabs.indexOf(Tab.entries[initialTab]).coerceAtLeast(0)
val pagerState = rememberPagerState(initialPage = initialPage, pageCount = { visibleTabs.size })

Box(modifier = Modifier.fillMaxSize()) {
Box(modifier = modifier.fillMaxSize()) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
beyondViewportPageCount = visibleTabs.size - 1,
userScrollEnabled = true,
) { page ->
val isCurrentPage = pagerState.currentPage == page
when (visibleTabs[page]) {
Tab.HOME -> {
val vm: HomeViewModel = viewModel(factory = VMFactory)
val installVm: InstallViewModel = viewModel(factory = VMFactory)
LaunchedEffect(Unit) { vm.startLoading() }
LaunchedEffect(isCurrentPage) {
if (isCurrentPage) vm.startLoading()
}
CollectNavEvents(vm, navigator)
CollectNavEvents(installVm, navigator)
HomeScreen(vm, installVm)
}
Tab.SUPERUSER -> {
val activity = LocalActivity.current as MainActivity
val vm: SuperuserViewModel = viewModel(viewModelStoreOwner = activity, factory = VMFactory)
LaunchedEffect(Unit) {
vm.authenticate = { onSuccess ->
activity.extension.withAuthentication { if (it) onSuccess() }
val activity = LocalActivity.current as? ComponentActivity
val vm: SuperuserViewModel = superuserViewModel
?: if (activity != null) {
viewModel(viewModelStoreOwner = activity, factory = VMFactory)
} else {
viewModel(factory = VMFactory)
}
LaunchedEffect(onAuthenticate) {
if (onAuthenticate != null) {
vm.authenticate = onAuthenticate
}
vm.startLoading()
}
LaunchedEffect(isCurrentPage) {
if (isCurrentPage) vm.startLoading()
}
SuperuserScreen(vm)
}
Tab.LOG -> {
val vm: LogViewModel = viewModel(factory = VMFactory)
LaunchedEffect(Unit) { vm.startLoading() }
LaunchedEffect(isCurrentPage) {
if (isCurrentPage) vm.startLoading()
}
LogScreen(vm)
}
Tab.MODULES -> {
val vm: ModuleViewModel = viewModel(factory = VMFactory)
LaunchedEffect(Unit) { vm.startLoading() }
LaunchedEffect(isCurrentPage) {
if (isCurrentPage) vm.startLoading()
}
CollectNavEvents(vm, navigator)
ModuleScreen(vm)
}
Tab.SETTINGS -> {
val activity = LocalActivity.current as MainActivity
val vm: SettingsViewModel = viewModel(factory = VMFactory)
LaunchedEffect(Unit) {
vm.authenticate = { onSuccess ->
activity.extension.withAuthentication { if (it) onSuccess() }
LaunchedEffect(onAuthenticate) {
if (onAuthenticate != null) {
vm.authenticate = onAuthenticate
}
}
CollectNavEvents(vm, navigator)
Expand Down
Loading
Loading