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.lifecycleOwner = viewLifecycleOwner
}
savedInstanceState?.let { viewModel.onRestoreState(it) }
return binding.root
}
override fun onSaveInstanceState(outState: Bundle) {
viewModel.onSaveState(outState)
}
override fun onStart() {
super.onStart()
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.WRITE_EXTERNAL_STORAGE
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.annotation.CallSuper
import androidx.databinding.Bindable
import androidx.databinding.Observable
@ -58,6 +59,9 @@ abstract class BaseViewModel(
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. */
@Synchronized
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.WRITE_EXTERNAL_STORAGE
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
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.RequestPermission
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.utils.RequestInstall
import com.topjohnwu.magisk.core.utils.UninstallPackage
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.ktx.reflectField
import com.topjohnwu.magisk.utils.Utils
import java.util.concurrent.CountDownLatch
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() {
private var permissionCallback: ((Boolean) -> Unit)? = null
@ -33,9 +45,9 @@ abstract class BaseActivity : AppCompatActivity() {
permissionCallback = null
}
private var contentCallback: ((Uri) -> Unit)? = null
private var contentCallback: ContentResultCallback? = null
private val getContent = registerForActivityResult(GetContent()) {
if (it != null) contentCallback?.invoke(it)
if (it != null) contentCallback?.onActivityResult(it)
contentCallback = null
}
@ -62,9 +74,17 @@ abstract class BaseActivity : AppCompatActivity() {
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
}
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
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) {
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 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
getContent.launch(type)
try {
getContent.launch(type)
callback.onActivityLaunch()
} catch (e: ActivityNotFoundException) {
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
}
}
@WorkerThread
@ -100,4 +125,8 @@ abstract class BaseActivity : AppCompatActivity() {
startActivity(Intent(intent).setFlags(0))
finish()
}
companion object {
private const val CONTENT_CALLBACK_KEY = "content_callback"
}
}

View File

@ -1,19 +1,13 @@
package com.topjohnwu.magisk.events
import android.content.ActivityNotFoundException
import android.content.Context
import android.net.Uri
import android.view.View
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.navigation.NavDirections
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.core.Const
import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.Shortcuts
@ -52,16 +46,12 @@ class RecreateEvent : ViewEvent(), ActivityExecutor {
}
}
class MagiskInstallFileEvent(
private val callback: (Uri) -> Unit
class GetContentEvent(
private val type: String,
private val callback: ContentResultCallback
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
try {
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)
}
activity.getContent(type, callback)
}
}
@ -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(
private val msg: TextHolder,
private val length: Int = Snackbar.LENGTH_SHORT,

View File

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

View File

@ -1,9 +1,5 @@
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.arch.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
@ -18,21 +14,4 @@ class InstallFragment : BaseFragment<FragmentInstallMd2Binding>() {
super.onStart()
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
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.widget.Toast
import androidx.databinding.Bindable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.di.AppContext
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.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.io.File
import java.io.IOException
@ -37,15 +46,15 @@ class InstallViewModel(
var step = if (skipOptions) 1 else 0
set(value) = set(value, field, { field = it }, BR.step)
var _method = -1
private var methodId = -1
@get:Bindable
var method
get() = _method
set(value) = set(value, _method, { _method = it }, BR.method) {
get() = methodId
set(value) = set(value, methodId, { methodId = it }, BR.method) {
when (it) {
R.id.method_patch -> {
MagiskInstallFileEvent { uri -> data = uri }.publish()
GetContentEvent("*/*", UriCallback()).publish()
}
R.id.method_inactive_slot -> {
SecondSlotWarningDialog().publish()
@ -53,9 +62,7 @@ class InstallViewModel(
}
}
@get:Bindable
var data: Uri? = null
set(value) = set(value, field, { field = it }, BR.data)
val data: LiveData<Uri?> get() = uri
@get:Bindable
var notes: Spanned = SpannableStringBuilder()
@ -81,17 +88,60 @@ class InstallViewModel(
}
}
fun step(nextStep: Int) {
step = nextStep
}
fun install() {
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_inactive_slot -> FlashFragment.flash(true).navigate(true)
else -> error("Unknown value")
}
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.view.View
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.adapterOf
@ -20,8 +22,12 @@ class ModuleFragment : BaseFragment<FragmentModuleMd2Binding>() {
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
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?) {

View File

@ -1,22 +1,26 @@
package com.topjohnwu.magisk.ui.module
import android.net.Uri
import androidx.lifecycle.MutableLiveData
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.Info
import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.diffListOf
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.dialog.ModuleInstallDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
class ModuleViewModel : BaseViewModel() {
@ -30,6 +34,8 @@ class ModuleViewModel : BaseViewModel() {
it.bindExtra(BR.viewModel, this)
}
val data get() = uri
init {
if (Info.env.isActive) {
items.insertItem(InstallModule)
@ -70,6 +76,18 @@ class ModuleViewModel : BaseViewModel() {
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}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.step(1)}"
android:onClick="@{() -> viewModel.setStep(1)}"
android:text="@string/install_next" />
</LinearLayout>