Handle Activty recreation on content result

Credits to @canyie for the initial PR and finding the bug
Close #5791, fix #5789
This commit is contained in:
topjohnwu 2022-05-08 14:29:59 -07:00
parent 0469f0b5ae
commit 4eb9240806
10 changed files with 140 additions and 72 deletions

View File

@ -36,9 +36,14 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
it.setVariable(BR.viewModel, viewModel) it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = viewLifecycleOwner it.lifecycleOwner = viewLifecycleOwner
} }
savedInstanceState?.let { viewModel.onRestoreState(it) }
return binding.root return binding.root
} }
override fun onSaveInstanceState(outState: Bundle) {
viewModel.onSaveState(outState)
}
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
activity?.supportActionBar?.subtitle = null activity?.supportActionBar?.subtitle = null

View File

@ -3,6 +3,7 @@ package com.topjohnwu.magisk.arch
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.databinding.Observable import androidx.databinding.Observable
@ -58,6 +59,9 @@ abstract class BaseViewModel(
isConnected.addOnPropertyChangedCallback(refreshCallback) isConnected.addOnPropertyChangedCallback(refreshCallback)
} }
open fun onSaveState(state: Bundle) {}
open fun onRestoreState(state: Bundle) {}
/** This should probably never be called manually, it's called manually via delegate. */ /** This should probably never be called manually, it's called manually via delegate. */
@Synchronized @Synchronized
fun requestRefresh() { fun requestRefresh() {

View File

@ -2,25 +2,37 @@ package com.topjohnwu.magisk.core.base
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts.GetContent import androidx.activity.result.contract.ActivityResultContracts.GetContent
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.utils.RequestInstall import com.topjohnwu.magisk.core.utils.RequestInstall
import com.topjohnwu.magisk.core.utils.UninstallPackage import com.topjohnwu.magisk.core.utils.UninstallPackage
import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.core.wrap import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.ktx.reflectField import com.topjohnwu.magisk.ktx.reflectField
import com.topjohnwu.magisk.utils.Utils
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
fun onActivityLaunch() {}
// Make the result type explicitly non-null
override fun onActivityResult(result: Uri)
}
abstract class BaseActivity : AppCompatActivity() { abstract class BaseActivity : AppCompatActivity() {
private var permissionCallback: ((Boolean) -> Unit)? = null private var permissionCallback: ((Boolean) -> Unit)? = null
@ -33,9 +45,9 @@ abstract class BaseActivity : AppCompatActivity() {
permissionCallback = null permissionCallback = null
} }
private var contentCallback: ((Uri) -> Unit)? = null private var contentCallback: ContentResultCallback? = null
private val getContent = registerForActivityResult(GetContent()) { private val getContent = registerForActivityResult(GetContent()) {
if (it != null) contentCallback?.invoke(it) if (it != null) contentCallback?.onActivityResult(it)
contentCallback = null contentCallback = null
} }
@ -62,9 +74,17 @@ abstract class BaseActivity : AppCompatActivity() {
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true) clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
clz.reflectField("mActivityHandlesUiMode").set(delegate, false) clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
} }
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
contentCallback?.let {
outState.putParcelable(CONTENT_CALLBACK_KEY, it)
}
}
fun withPermission(permission: String, callback: (Boolean) -> Unit) { fun withPermission(permission: String, callback: (Boolean) -> Unit) {
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) { if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
// We do not need external rw on 30+ // We do not need external rw on 30+
@ -79,9 +99,14 @@ abstract class BaseActivity : AppCompatActivity() {
} }
} }
fun getContent(type: String, callback: (Uri) -> Unit) { fun getContent(type: String, callback: ContentResultCallback) {
contentCallback = callback contentCallback = callback
getContent.launch(type) try {
getContent.launch(type)
callback.onActivityLaunch()
} catch (e: ActivityNotFoundException) {
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
}
} }
@WorkerThread @WorkerThread
@ -100,4 +125,8 @@ abstract class BaseActivity : AppCompatActivity() {
startActivity(Intent(intent).setFlags(0)) startActivity(Intent(intent).setFlags(0))
finish() finish()
} }
companion object {
private const val CONTENT_CALLBACK_KEY = "content_callback"
}
} }

View File

@ -1,19 +1,13 @@
package com.topjohnwu.magisk.events package com.topjohnwu.magisk.events
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.net.Uri
import android.view.View import android.view.View
import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.* import com.topjohnwu.magisk.arch.*
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.utils.TextHolder import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asText import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
@ -52,16 +46,12 @@ class RecreateEvent : ViewEvent(), ActivityExecutor {
} }
} }
class MagiskInstallFileEvent( class GetContentEvent(
private val callback: (Uri) -> Unit private val type: String,
private val callback: ContentResultCallback
) : ViewEvent(), ActivityExecutor { ) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) { override fun invoke(activity: UIActivity<*>) {
try { activity.getContent(type, callback)
activity.getContent("*/*", callback)
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
} catch (e: ActivityNotFoundException) {
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
}
} }
} }
@ -83,20 +73,6 @@ class AddHomeIconEvent : ViewEvent(), ContextExecutor {
} }
} }
class SelectModuleEvent : ViewEvent(), FragmentExecutor {
override fun invoke(fragment: BaseFragment<*>) {
try {
fragment.apply {
activity?.getContent("application/zip") {
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, it).navigate()
}
}
} catch (e: ActivityNotFoundException) {
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
}
}
}
class SnackbarEvent( class SnackbarEvent(
private val msg: TextHolder, private val msg: TextHolder,
private val length: Int = Snackbar.LENGTH_SHORT, private val length: Int = Snackbar.LENGTH_SHORT,

View File

@ -53,7 +53,8 @@ class FlashViewModel : BaseViewModel() {
viewModelScope.launch { viewModelScope.launch {
val result = when (action) { val result = when (action) {
Const.Value.FLASH_ZIP -> { Const.Value.FLASH_ZIP -> {
FlashZip(uri!!, outItems, logItems).exec() uri ?: return@launch
FlashZip(uri, outItems, logItems).exec()
} }
Const.Value.UNINSTALL -> { Const.Value.UNINSTALL -> {
showReboot = false showReboot = false

View File

@ -1,9 +1,5 @@
package com.topjohnwu.magisk.ui.install package com.topjohnwu.magisk.ui.install
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
@ -18,21 +14,4 @@ class InstallFragment : BaseFragment<FragmentInstallMd2Binding>() {
super.onStart() super.onStart()
requireActivity().setTitle(R.string.install) requireActivity().setTitle(R.string.install)
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel._method = savedInstanceState?.getInt(KEY_CURRENT_METHOD, -1) ?: -1
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(KEY_CURRENT_METHOD, viewModel.method)
}
companion object {
private const val KEY_CURRENT_METHOD = "current_method"
}
} }

View File

@ -1,25 +1,34 @@
package com.topjohnwu.magisk.ui.install package com.topjohnwu.magisk.ui.install
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.widget.Toast
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
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.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.ContentResultCallback
import com.topjohnwu.magisk.data.repository.NetworkService import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.databinding.set import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.di.AppContext import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.events.MagiskInstallFileEvent import com.topjohnwu.magisk.events.GetContentEvent
import com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog import com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog
import com.topjohnwu.magisk.ui.flash.FlashFragment import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.Utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -37,15 +46,15 @@ class InstallViewModel(
var step = if (skipOptions) 1 else 0 var step = if (skipOptions) 1 else 0
set(value) = set(value, field, { field = it }, BR.step) set(value) = set(value, field, { field = it }, BR.step)
var _method = -1 private var methodId = -1
@get:Bindable @get:Bindable
var method var method
get() = _method get() = methodId
set(value) = set(value, _method, { _method = it }, BR.method) { set(value) = set(value, methodId, { methodId = it }, BR.method) {
when (it) { when (it) {
R.id.method_patch -> { R.id.method_patch -> {
MagiskInstallFileEvent { uri -> data = uri }.publish() GetContentEvent("*/*", UriCallback()).publish()
} }
R.id.method_inactive_slot -> { R.id.method_inactive_slot -> {
SecondSlotWarningDialog().publish() SecondSlotWarningDialog().publish()
@ -53,9 +62,7 @@ class InstallViewModel(
} }
} }
@get:Bindable val data: LiveData<Uri?> get() = uri
var data: Uri? = null
set(value) = set(value, field, { field = it }, BR.data)
@get:Bindable @get:Bindable
var notes: Spanned = SpannableStringBuilder() var notes: Spanned = SpannableStringBuilder()
@ -81,17 +88,60 @@ class InstallViewModel(
} }
} }
fun step(nextStep: Int) {
step = nextStep
}
fun install() { fun install() {
when (method) { when (method) {
R.id.method_patch -> FlashFragment.patch(data!!).navigate(true) R.id.method_patch -> FlashFragment.patch(data.value!!).navigate(true)
R.id.method_direct -> FlashFragment.flash(false).navigate(true) R.id.method_direct -> FlashFragment.flash(false).navigate(true)
R.id.method_inactive_slot -> FlashFragment.flash(true).navigate(true) R.id.method_inactive_slot -> FlashFragment.flash(true).navigate(true)
else -> error("Unknown value") else -> error("Unknown value")
} }
state = State.LOADING state = State.LOADING
} }
override fun onSaveState(state: Bundle) {
state.putParcelable(INSTALL_STATE_KEY, InstallState(
methodId,
step,
Config.keepVerity,
Config.keepEnc,
Config.patchVbmeta,
Config.recovery
))
}
override fun onRestoreState(state: Bundle) {
state.getParcelable<InstallState>(INSTALL_STATE_KEY)?.let {
methodId = it.method
step = it.step
Config.keepVerity = it.keepVerity
Config.keepEnc = it.keepEnc
Config.patchVbmeta = it.patchVbmeta
Config.recovery = it.recovery
}
}
@Parcelize
class UriCallback : ContentResultCallback {
override fun onActivityLaunch() {
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
}
override fun onActivityResult(result: Uri) {
uri.value = result
}
}
@Parcelize
class InstallState(
val method: Int,
val step: Int,
val keepVerity: Boolean,
val keepEnc: Boolean,
val patchVbmeta: Boolean,
val recovery: Boolean,
) : Parcelable
companion object {
private const val INSTALL_STATE_KEY = "install_state"
private val uri = MutableLiveData<Uri?>()
}
} }

View File

@ -2,8 +2,10 @@ package com.topjohnwu.magisk.ui.module
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import com.topjohnwu.magisk.databinding.RvItem import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.adapterOf import com.topjohnwu.magisk.databinding.adapterOf
@ -20,8 +22,12 @@ class ModuleFragment : BaseFragment<FragmentModuleMd2Binding>() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
setHasOptionsMenu(true)
activity?.title = resources.getString(R.string.modules) activity?.title = resources.getString(R.string.modules)
viewModel.data.observe(this) {
it ?: return@observe
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, it).navigate()
viewModel.data.value = null
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@ -1,22 +1,26 @@
package com.topjohnwu.magisk.ui.module package com.topjohnwu.magisk.ui.module
import android.net.Uri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
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.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.core.model.module.LocalModule import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.model.module.OnlineModule import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.databinding.RvItem import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.diffListOf import com.topjohnwu.magisk.databinding.diffListOf
import com.topjohnwu.magisk.databinding.itemBindingOf import com.topjohnwu.magisk.databinding.itemBindingOf
import com.topjohnwu.magisk.events.SelectModuleEvent import com.topjohnwu.magisk.events.GetContentEvent
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
class ModuleViewModel : BaseViewModel() { class ModuleViewModel : BaseViewModel() {
@ -30,6 +34,8 @@ class ModuleViewModel : BaseViewModel() {
it.bindExtra(BR.viewModel, this) it.bindExtra(BR.viewModel, this)
} }
val data get() = uri
init { init {
if (Info.env.isActive) { if (Info.env.isActive) {
items.insertItem(InstallModule) items.insertItem(InstallModule)
@ -70,6 +76,18 @@ class ModuleViewModel : BaseViewModel() {
SnackbarEvent(R.string.no_connection).publish() SnackbarEvent(R.string.no_connection).publish()
} }
fun installPressed() = withExternalRW { SelectModuleEvent().publish() } fun installPressed() = withExternalRW {
GetContentEvent("application/zip", UriCallback()).publish()
}
@Parcelize
class UriCallback : ContentResultCallback {
override fun onActivityResult(result: Uri) {
uri.value = result
}
}
companion object {
private val uri = MutableLiveData<Uri?>()
}
} }

View File

@ -72,7 +72,7 @@
gone="@{viewModel.step != 0}" gone="@{viewModel.step != 0}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.step(1)}" android:onClick="@{() -> viewModel.setStep(1)}"
android:text="@string/install_next" /> android:text="@string/install_next" />
</LinearLayout> </LinearLayout>