diff --git a/app/src/main/java/com/topjohnwu/magisk/arch/BaseFragment.kt b/app/src/main/java/com/topjohnwu/magisk/arch/BaseFragment.kt index 7a8ff6cae..02d000f0e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/arch/BaseFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/arch/BaseFragment.kt @@ -36,9 +36,14 @@ abstract class BaseFragment : 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 diff --git a/app/src/main/java/com/topjohnwu/magisk/arch/BaseViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/arch/BaseViewModel.kt index 6e26454ad..0f28275f1 100644 --- a/app/src/main/java/com/topjohnwu/magisk/arch/BaseViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/arch/BaseViewModel.kt @@ -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() { diff --git a/app/src/main/java/com/topjohnwu/magisk/core/base/BaseActivity.kt b/app/src/main/java/com/topjohnwu/magisk/core/base/BaseActivity.kt index ae0d0e131..d18e220ad 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/base/BaseActivity.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/base/BaseActivity.kt @@ -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, 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" + } } diff --git a/app/src/main/java/com/topjohnwu/magisk/events/ViewEvents.kt b/app/src/main/java/com/topjohnwu/magisk/events/ViewEvents.kt index 3421e4ebf..77a91d49e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/events/ViewEvents.kt +++ b/app/src/main/java/com/topjohnwu/magisk/events/ViewEvents.kt @@ -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, diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt index cfbd5d041..15c1b4618 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt @@ -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 diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt index aad114f83..bbf8e1e93 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt @@ -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() { 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" - } } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt index 7be546ac2..5541f292e 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt @@ -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 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(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() + } } diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt index 4eadb64a4..ee4797782 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt @@ -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() { 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?) { diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt index 7d9a67abc..d7b2668fc 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt @@ -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() + } } diff --git a/app/src/main/res/layout/fragment_install_md2.xml b/app/src/main/res/layout/fragment_install_md2.xml index 59a8fd06b..64bd12709 100644 --- a/app/src/main/res/layout/fragment_install_md2.xml +++ b/app/src/main/res/layout/fragment_install_md2.xml @@ -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" />