diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt index 7231a9b9f..8ba6c2c83 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt @@ -5,75 +5,88 @@ import android.content.Context import android.content.pm.ActivityInfo import android.net.Uri import android.os.Bundle -import android.view.KeyEvent -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem +import android.view.LayoutInflater import android.view.View -import androidx.core.view.MenuProvider -import androidx.core.view.isVisible +import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import androidx.navigation.NavDeepLinkBuilder import com.topjohnwu.magisk.MainDirections import com.topjohnwu.magisk.R -import com.topjohnwu.magisk.arch.BaseFragment -import com.topjohnwu.magisk.arch.viewModel +import com.topjohnwu.magisk.arch.ActivityExecutor +import com.topjohnwu.magisk.arch.ContextExecutor +import com.topjohnwu.magisk.arch.NavigationActivity +import com.topjohnwu.magisk.arch.UIActivity +import com.topjohnwu.magisk.arch.VMFactory +import com.topjohnwu.magisk.arch.ViewEvent +import com.topjohnwu.magisk.arch.ViewModelHolder import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.cmp -import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding import com.topjohnwu.magisk.ui.MainActivity +import com.topjohnwu.magisk.ui.theme.MagiskTheme import com.topjohnwu.magisk.core.R as CoreR -class FlashFragment : BaseFragment(), MenuProvider { +class FlashFragment : Fragment(), ViewModelHolder { - override val layoutRes = R.layout.fragment_flash_md2 - override val viewModel by viewModel() - override val snackbarView: View get() = binding.snackbarContainer - override val snackbarAnchorView: View? - get() = if (binding.restartBtn.isShown) binding.restartBtn else super.snackbarAnchorView + override val viewModel by lazy { + ViewModelProvider(this, VMFactory)[FlashViewModel::class.java] + } private var defaultOrientation = -1 + private val backCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if ((viewModel as FlashViewModel).flashing.value != true) { + isEnabled = false + activity?.onBackPressedDispatcher?.onBackPressed() + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel.args = FlashFragmentArgs.fromBundle(requireArguments()) + startObserveLiveData() + (viewModel as FlashViewModel).args = FlashFragmentArgs.fromBundle(requireArguments()) + activity?.onBackPressedDispatcher?.addCallback(this, backCallback) } override fun onStart() { super.onStart() - activity?.setTitle(CoreR.string.flash_screen_title) + (activity as? NavigationActivity<*>)?.setTitle(CoreR.string.flash_screen_title) - viewModel.state.observe(this) { - activity?.supportActionBar?.setSubtitle( + (viewModel as FlashViewModel).state.observe(this) { + (activity as? androidx.appcompat.app.AppCompatActivity)?.supportActionBar?.setSubtitle( when (it) { FlashViewModel.State.FLASHING -> CoreR.string.flashing FlashViewModel.State.SUCCESS -> CoreR.string.done FlashViewModel.State.FAILED -> CoreR.string.failure } ) - if (it == FlashViewModel.State.SUCCESS && viewModel.showReboot) { - binding.restartBtn.apply { - if (!this.isVisible) this.show() - if (!this.isFocused) this.requestFocus() - } - } } } - override fun onCreateMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.menu_flash, menu) - } - - override fun onMenuItemSelected(item: MenuItem): Boolean { - return viewModel.onMenuItemClicked(item) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { defaultOrientation = activity?.requestedOrientation ?: -1 activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED + if (savedInstanceState == null) { - viewModel.startFlashing() + (viewModel as FlashViewModel).startFlashing() + } + + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + MagiskTheme { + FlashScreen(viewModel = viewModel as FlashViewModel) + } + } } } @@ -85,22 +98,13 @@ class FlashFragment : BaseFragment(), MenuProvider { super.onDestroyView() } - override fun onKeyEvent(event: KeyEvent): Boolean { - return when (event.keyCode) { - KeyEvent.KEYCODE_VOLUME_UP, - KeyEvent.KEYCODE_VOLUME_DOWN -> true - else -> false + override fun onEventDispatched(event: ViewEvent) { + when (event) { + is ContextExecutor -> event(requireContext()) + is ActivityExecutor -> (activity as? UIActivity<*>)?.let { event(it) } } } - override fun onBackPressed(): Boolean { - if (viewModel.flashing.value == true) - return true - return super.onBackPressed() - } - - override fun onPreBind(binding: FragmentFlashMd2Binding) = Unit - companion object { private fun createIntent(context: Context, args: FlashFragmentArgs) = @@ -114,27 +118,19 @@ class FlashFragment : BaseFragment(), MenuProvider { private fun flashType(isSecondSlot: Boolean) = if (isSecondSlot) Const.Value.FLASH_INACTIVE_SLOT else Const.Value.FLASH_MAGISK - /* Flashing is understood as installing / flashing magisk itself */ - fun flash(isSecondSlot: Boolean) = MainDirections.actionFlashFragment( action = flashType(isSecondSlot) ) - /* Patching is understood as injecting img files with magisk */ - fun patch(uri: Uri) = MainDirections.actionFlashFragment( action = Const.Value.PATCH_FILE, additionalData = uri ) - /* Uninstalling is understood as removing magisk entirely */ - fun uninstall() = MainDirections.actionFlashFragment( action = Const.Value.UNINSTALL ) - /* Installing is understood as flashing modules / zips */ - fun installIntent(context: Context, file: Uri) = FlashFragmentArgs( action = Const.Value.FLASH_ZIP, additionalData = file, @@ -145,5 +141,4 @@ class FlashFragment : BaseFragment(), MenuProvider { additionalData = file, ) } - } diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashScreen.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashScreen.kt new file mode 100644 index 000000000..fccce81e2 --- /dev/null +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashScreen.kt @@ -0,0 +1,85 @@ +package com.topjohnwu.magisk.ui.flash + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import top.yukonga.miuix.kmp.basic.FloatingActionButton +import top.yukonga.miuix.kmp.basic.Text +import top.yukonga.miuix.kmp.basic.TextButton +import top.yukonga.miuix.kmp.theme.MiuixTheme +import com.topjohnwu.magisk.core.R as CoreR + +@Composable +fun FlashScreen(viewModel: FlashViewModel) { + val flashState by viewModel.flashState.collectAsState() + val showReboot by viewModel.showReboot.collectAsState() + val items = viewModel.consoleItems + val listState = rememberLazyListState() + + LaunchedEffect(items.size) { + if (items.isNotEmpty()) { + listState.animateScrollToItem(items.size - 1) + } + } + + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + itemsIndexed(items) { _, line -> + Text( + text = line, + fontFamily = FontFamily.Monospace, + fontSize = 12.sp, + lineHeight = 16.sp, + color = MiuixTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth() + ) + } + } + + if (flashState != FlashViewModel.State.FLASHING) { + TextButton( + text = stringResource(CoreR.string.menuSaveLog), + onClick = { viewModel.saveLog() }, + modifier = Modifier + .align(Alignment.BottomStart) + .padding(16.dp) + ) + } + + if (flashState == FlashViewModel.State.SUCCESS && showReboot) { + FloatingActionButton( + onClick = { viewModel.restartPressed() }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp) + ) { + Text( + text = stringResource(CoreR.string.reboot), + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + } + } +} diff --git a/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt b/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt index 56704d869..5a825f7fc 100644 --- a/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt +++ b/app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt @@ -1,14 +1,10 @@ package com.topjohnwu.magisk.ui.flash -import android.view.MenuItem -import androidx.databinding.Bindable -import androidx.databinding.ObservableArrayList +import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.map import androidx.lifecycle.viewModelScope -import com.topjohnwu.magisk.BR -import com.topjohnwu.magisk.R import com.topjohnwu.magisk.arch.BaseViewModel import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Info @@ -20,10 +16,12 @@ import com.topjohnwu.magisk.core.tasks.FlashZip import com.topjohnwu.magisk.core.tasks.MagiskInstaller import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream -import com.topjohnwu.magisk.databinding.set import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.superuser.CallbackList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class FlashViewModel : BaseViewModel() { @@ -36,18 +34,20 @@ class FlashViewModel : BaseViewModel() { val state: LiveData get() = _state val flashing = state.map { it == State.FLASHING } - @get:Bindable - var showReboot = Info.isRooted - set(value) = set(value, field, { field = it }, BR.showReboot) + private val _flashState = MutableStateFlow(State.FLASHING) + val flashState: StateFlow = _flashState.asStateFlow() - val items = ObservableArrayList() + private val _showReboot = MutableStateFlow(Info.isRooted) + val showReboot: StateFlow = _showReboot.asStateFlow() + + val consoleItems = mutableStateListOf() lateinit var args: FlashFragmentArgs private val logItems = mutableListOf().synchronized() private val outItems = object : CallbackList() { override fun onAddElement(e: String?) { e ?: return - items.add(ConsoleItem(e)) + consoleItems.add(e) logItems.add(e) } } @@ -62,7 +62,7 @@ class FlashViewModel : BaseViewModel() { FlashZip(uri, outItems, logItems).exec() } Const.Value.UNINSTALL -> { - showReboot = false + _showReboot.value = false MagiskInstaller.Uninstall(outItems, logItems).exec() } Const.Value.FLASH_MAGISK -> { @@ -72,12 +72,12 @@ class FlashViewModel : BaseViewModel() { MagiskInstaller.Direct(outItems, logItems).exec() } Const.Value.FLASH_INACTIVE_SLOT -> { - showReboot = false + _showReboot.value = false MagiskInstaller.SecondSlot(outItems, logItems).exec() } Const.Value.PATCH_FILE -> { uri ?: return@launch - showReboot = false + _showReboot.value = false MagiskInstaller.Patch(uri, outItems, logItems).exec() } else -> { @@ -90,17 +90,12 @@ class FlashViewModel : BaseViewModel() { } private fun onResult(success: Boolean) { - _state.value = if (success) State.SUCCESS else State.FAILED + val newState = if (success) State.SUCCESS else State.FAILED + _state.value = newState + _flashState.value = newState } - fun onMenuItemClicked(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_save -> savePressed() - } - return true - } - - private fun savePressed() = withExternalRW { + fun saveLog() = withExternalRW { viewModelScope.launch(Dispatchers.IO) { val name = "magisk_install_log_%s.log".format( System.currentTimeMillis().toTime(timeFormatStandard) diff --git a/app/apk/src/main/res/layout/fragment_flash_md2.xml b/app/apk/src/main/res/layout/fragment_flash_md2.xml deleted file mode 100644 index 6ce2ff1f8..000000000 --- a/app/apk/src/main/res/layout/fragment_flash_md2.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/apk/src/main/res/navigation/main.xml b/app/apk/src/main/res/navigation/main.xml index 3b270a601..4ac41e37b 100644 --- a/app/apk/src/main/res/navigation/main.xml +++ b/app/apk/src/main/res/navigation/main.xml @@ -28,8 +28,7 @@ + android:label="FlashFragment">