Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup: Improve root setup display and interaction #833

Merged
merged 2 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<RootServiceClient.Connection?> = settings.useRoot.flow
.flatMapLatest {
if (it != true) return@flatMapLatest emptyFlow()

callbackFlow<RootServiceClient.Connection?> {
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()

Expand Down Expand Up @@ -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"
)
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/eu/darken/sdmse/setup/SetupViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/eu/darken/sdmse/setup/root/RootSetupCardVH.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,6 +19,18 @@ class RootSetupCardVH(parent: ViewGroup) :
payloads: List<Any>
) -> 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) {
Expand Down
41 changes: 34 additions & 7 deletions app/src/main/java/eu/darken/sdmse/setup/root/RootSetupModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<SetupModule.State> = 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() {
Expand All @@ -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
Expand Down
19 changes: 16 additions & 3 deletions app/src/main/res/layout/setup_root_item.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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" />

<com.google.android.material.textview.MaterialTextView
android:id="@+id/root_state"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="wrap_content"
android:layout_marginTop="16dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:text="@string/setup_root_state_waiting_label"
app:layout_constraintBottom_toTopOf="@id/allow_root_options"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/body" />

<RadioGroup
android:id="@+id/allow_root_options"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
app:layout_constraintTop_toBottomOf="@id/body">
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/root_state">

<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/allow_root_options_enable"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@

<string name="setup_root_card_title">Root access</string>
<string name="setup_root_card_body">Should SD Maid use root access if it is available? Root access can be used to check additional system folders and private application data.</string>
<string name="setup_root_state_ready_label">Root access is ready.</string>
<string name="setup_root_state_waiting_label">Waiting for root access.</string>
<string name="setup_root_enable_root_use_label">Use root access if available</string>
<string name="setup_root_disable_root_use_label">Don\'t use root access</string>
<string name="setup_root_card_body2">Only available on rooted devices. If you didn\'t make this modification, then your device is not rooted and this setting has no effect.</string>
Expand Down