mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-07-03 14:08:39 +02:00
Migrate Flash screen to Jetpack Compose with miuix
Replace Fragment data-binding UI with a Compose-based FlashScreen using LazyColumn for monospace console output with auto-scroll, a save log TextButton, and miuix FloatingActionButton for reboot. FlashViewModel now uses mutableStateListOf for console lines and StateFlow for flash state. Back press blocking uses OnBackPressedCallback instead of BaseFragment override. Made-with: Cursor
This commit is contained in:
@@ -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<FragmentFlashMd2Binding>(), MenuProvider {
|
||||
class FlashFragment : Fragment(), ViewModelHolder {
|
||||
|
||||
override val layoutRes = R.layout.fragment_flash_md2
|
||||
override val viewModel by viewModel<FlashViewModel>()
|
||||
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<FragmentFlashMd2Binding>(), 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<FragmentFlashMd2Binding>(), 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<FragmentFlashMd2Binding>(), MenuProvider {
|
||||
additionalData = file,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<State> 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<State> = _flashState.asStateFlow()
|
||||
|
||||
val items = ObservableArrayList<ConsoleItem>()
|
||||
private val _showReboot = MutableStateFlow(Info.isRooted)
|
||||
val showReboot: StateFlow<Boolean> = _showReboot.asStateFlow()
|
||||
|
||||
val consoleItems = mutableStateListOf<String>()
|
||||
lateinit var args: FlashFragmentArgs
|
||||
|
||||
private val logItems = mutableListOf<String>().synchronized()
|
||||
private val outItems = object : CallbackList<String>() {
|
||||
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)
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="com.topjohnwu.magisk.ui.flash.FlashViewModel" />
|
||||
|
||||
</data>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="@dimen/internal_action_bar_size"
|
||||
app:layout_fitsSystemWindowsInsets="top"
|
||||
tools:layout_marginTop="@dimen/internal_action_bar_size">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/flash_content"
|
||||
scrollToLast="@{true}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
app:fitsSystemWindowsInsets="start|end|bottom"
|
||||
app:items="@{viewModel.items}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:listitem="@layout/item_console_md2" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/restart_btn"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/l1"
|
||||
android:layout_marginBottom="@dimen/l1"
|
||||
android:clickable="@{!viewModel.flashing}"
|
||||
android:enabled="@{!viewModel.flashing}"
|
||||
android:focusable="true"
|
||||
android:onClick="@{() -> viewModel.restartPressed()}"
|
||||
android:text="@string/reboot"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="?colorOnPrimary"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="?colorPrimary"
|
||||
app:icon="@drawable/ic_restart"
|
||||
app:iconTint="?colorOnPrimary"
|
||||
app:layout_fitsSystemWindowsInsets="bottom" />
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/snackbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:fitsSystemWindowsInsets="top|bottom" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</layout>
|
||||
@@ -28,8 +28,7 @@
|
||||
<fragment
|
||||
android:id="@+id/flashFragment"
|
||||
android:name="com.topjohnwu.magisk.ui.flash.FlashFragment"
|
||||
android:label="FlashFragment"
|
||||
tools:layout="@layout/fragment_flash_md2">
|
||||
android:label="FlashFragment">
|
||||
|
||||
<argument
|
||||
android:name="action"
|
||||
|
||||
Reference in New Issue
Block a user