From 1bec656693bd84d5651e5634c759ed24d984c00b Mon Sep 17 00:00:00 2001 From: darken Date: Wed, 29 Nov 2023 14:13:34 +0100 Subject: [PATCH 1/2] Root: Display a "link state" similar to how the Shizuku setup card behaves --- .../darken/sdmse/common/root/RootManager.kt | 50 +++++++++++++++++++ .../sdmse/setup/root/RootSetupCardVH.kt | 14 ++++++ .../sdmse/setup/root/RootSetupModule.kt | 41 ++++++++++++--- app/src/main/res/layout/setup_root_item.xml | 19 +++++-- app/src/main/res/values/strings.xml | 2 + 5 files changed, 116 insertions(+), 10 deletions(-) diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/root/RootManager.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/root/RootManager.kt index e3fead184..836c06769 100644 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/root/RootManager.kt +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/root/RootManager.kt @@ -1,14 +1,24 @@ package eu.darken.sdmse.common.root +import android.content.Context +import android.content.pm.PackageManager +import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.sdmse.common.coroutine.AppScope import eu.darken.sdmse.common.coroutine.DispatcherProvider import eu.darken.sdmse.common.debug.logging.Logging.Priority.WARN import eu.darken.sdmse.common.debug.logging.log import eu.darken.sdmse.common.debug.logging.logTag +import eu.darken.sdmse.common.flow.replayingShare +import eu.darken.sdmse.common.flow.setupCommonEventHandlers import eu.darken.sdmse.common.flow.shareLatest import eu.darken.sdmse.common.root.service.RootServiceClient import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.sync.Mutex @@ -19,12 +29,33 @@ import javax.inject.Singleton @Singleton class RootManager @Inject constructor( + @ApplicationContext private val context: Context, @AppScope private val appScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, val serviceClient: RootServiceClient, settings: RootSettings, ) { + val binder: Flow = settings.useRoot.flow + .flatMapLatest { + if (it != true) return@flatMapLatest emptyFlow() + + callbackFlow { + val resource = serviceClient.get() + send(resource.item) + awaitClose { + log(TAG) { "Closing binder resource" } + resource.close() + } + } + } + .catch { + log(TAG, WARN) { "RootServiceClient.Connection was unavailable" } + emit(null) + } + .setupCommonEventHandlers(TAG) { "binder" } + .replayingShare(appScope) + private var cachedState: Boolean? = null private val cacheLock = Mutex() @@ -64,7 +95,26 @@ class RootManager @Inject constructor( .mapLatest { (it ?: false) && isRooted() } .shareLatest(appScope) + suspend fun isInstalled(): Boolean { + val installed = + KNOWN_ROOT_MANAGERS.any { + try { + @Suppress("DEPRECATION") + context.packageManager.getPackageInfo(it, 0) + true + } catch (e: PackageManager.NameNotFoundException) { + false + } + } + + log(TAG) { "isInstalled(): $installed" } + return installed + } + companion object { internal val TAG = logTag("Root", "Manager") + private val KNOWN_ROOT_MANAGERS = setOf( + "com.topjohnwu.magisk" + ) } } diff --git a/app/src/main/java/eu/darken/sdmse/setup/root/RootSetupCardVH.kt b/app/src/main/java/eu/darken/sdmse/setup/root/RootSetupCardVH.kt index fff7dec46..c5286f29d 100644 --- a/app/src/main/java/eu/darken/sdmse/setup/root/RootSetupCardVH.kt +++ b/app/src/main/java/eu/darken/sdmse/setup/root/RootSetupCardVH.kt @@ -1,7 +1,9 @@ package eu.darken.sdmse.setup.root import android.view.ViewGroup +import androidx.core.view.isVisible import eu.darken.sdmse.R +import eu.darken.sdmse.common.getColorForAttr import eu.darken.sdmse.common.lists.binding import eu.darken.sdmse.databinding.SetupRootItemBinding import eu.darken.sdmse.setup.SetupAdapter @@ -17,6 +19,18 @@ class RootSetupCardVH(parent: ViewGroup) : payloads: List ) -> Unit = binding { item -> + rootState.apply { + isVisible = item.state.useRoot == true && item.state.isInstalled + text = getString( + if (item.state.ourService) R.string.setup_root_state_ready_label + else R.string.setup_root_state_waiting_label + ) + setTextColor( + if (item.state.ourService) context.getColorForAttr(android.R.attr.textColorSecondary) + else context.getColorForAttr(android.R.attr.colorError) + ) + } + allowRootOptions.apply { setOnCheckedChangeListener(null) when (item.state.useRoot) { diff --git a/app/src/main/java/eu/darken/sdmse/setup/root/RootSetupModule.kt b/app/src/main/java/eu/darken/sdmse/setup/root/RootSetupModule.kt index ce3bc8bf8..9f4bdccd5 100644 --- a/app/src/main/java/eu/darken/sdmse/setup/root/RootSetupModule.kt +++ b/app/src/main/java/eu/darken/sdmse/setup/root/RootSetupModule.kt @@ -8,6 +8,7 @@ import dagger.multibindings.IntoSet import eu.darken.sdmse.common.areas.DataAreaManager import eu.darken.sdmse.common.coroutine.AppScope import eu.darken.sdmse.common.datastore.value +import eu.darken.sdmse.common.debug.logging.Logging.Priority.WARN import eu.darken.sdmse.common.debug.logging.log import eu.darken.sdmse.common.debug.logging.logTag import eu.darken.sdmse.common.flow.replayingShare @@ -16,11 +17,16 @@ import eu.darken.sdmse.common.root.RootManager import eu.darken.sdmse.common.root.RootSettings import eu.darken.sdmse.setup.SetupModule import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.take import javax.inject.Inject import javax.inject.Singleton @@ -34,12 +40,31 @@ class RootSetupModule @Inject constructor( ) : SetupModule { private val refreshTrigger = MutableStateFlow(rngString) - override val state = refreshTrigger - .mapLatest { - return@mapLatest State( - useRoot = rootSettings.useRoot.value(), - ) - } + override val state: Flow = combine(refreshTrigger, rootSettings.useRoot.flow) { _, useRoot -> + val baseState = State( + useRoot = useRoot, + isInstalled = rootManager.isInstalled(), + ) + + if (useRoot != true) return@combine flowOf(baseState) + + rootManager.binder + .onStart { emit(null) } + .map { connection -> + if (connection == null) return@map baseState + + baseState.copy( + ourService = try { + connection.ipc.checkBase() != null + } catch (e: Exception) { + log(TAG, WARN) { "Error while checking for root: $e" } + false + }, + ) + } + } + .flatMapLatest { it } + .onEach { log(TAG) { "New Root setup state: $it" } } .replayingShare(appScope) override suspend fun refresh() { @@ -65,6 +90,8 @@ class RootSetupModule @Inject constructor( data class State( val useRoot: Boolean?, + val isInstalled: Boolean = false, + val ourService: Boolean = false, ) : SetupModule.State { override val type: SetupModule.Type diff --git a/app/src/main/res/layout/setup_root_item.xml b/app/src/main/res/layout/setup_root_item.xml index aa5c0c765..2d72d054a 100644 --- a/app/src/main/res/layout/setup_root_item.xml +++ b/app/src/main/res/layout/setup_root_item.xml @@ -41,18 +41,31 @@ android:layout_marginHorizontal="16dp" android:layout_marginTop="8dp" android:text="@string/setup_root_card_body" - app:layout_constraintBottom_toTopOf="@id/allow_root_options" + app:layout_constraintBottom_toTopOf="@id/root_state" app:layout_constraintEnd_toEndOf="@id/title" app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintTop_toBottomOf="@id/title" /> + + + android:layout_marginBottom="8dp" + app:layout_constraintTop_toBottomOf="@id/root_state"> Root access Should SD Maid use root access if it is available? Root access can be used to check additional system folders and private application data. + Root access is ready. + Waiting for root access. Use root access if available Don\'t use root access Only available on rooted devices. If you didn\'t make this modification, then your device is not rooted and this setting has no effect. From feb911933c4fff2019d4c60e178eb1ae74443c92 Mon Sep 17 00:00:00 2001 From: darken Date: Wed, 29 Nov 2023 14:21:41 +0100 Subject: [PATCH 2/2] Setup: If we have detected a root app and the user has not made a yes/no decision, put the root card at the top, granting it will automatically finish most other setup steps. --- app/src/main/java/eu/darken/sdmse/setup/SetupViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/eu/darken/sdmse/setup/SetupViewModel.kt b/app/src/main/java/eu/darken/sdmse/setup/SetupViewModel.kt index bfc9099f5..e4428e790 100644 --- a/app/src/main/java/eu/darken/sdmse/setup/SetupViewModel.kt +++ b/app/src/main/java/eu/darken/sdmse/setup/SetupViewModel.kt @@ -169,6 +169,8 @@ class SetupViewModel @Inject constructor( .sortedBy { item -> if (screenOptions.showCompleted && !item.state.isComplete) { Int.MIN_VALUE + } else if (item is RootSetupCardVH.Item && item.state.isInstalled && item.state.useRoot == null) { + Int.MIN_VALUE } else { DISPLAY_ORDER.indexOfFirst { it.isInstance(item) } }