Update UI for sharedUID support

This commit is contained in:
topjohnwu 2022-03-25 16:56:21 -07:00
parent 9f1740cc4f
commit 31f88e0f05
7 changed files with 126 additions and 121 deletions

View File

@ -2,9 +2,7 @@ package com.topjohnwu.magisk.core.magiskdb
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.model.su.SuPolicy import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.core.model.su.createPolicy
import com.topjohnwu.magisk.di.AppContext import com.topjohnwu.magisk.di.AppContext
import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class PolicyDao : MagiskDB() { class PolicyDao : MagiskDB() {
@ -23,28 +21,31 @@ class PolicyDao : MagiskDB() {
suspend fun fetch(uid: Int): SuPolicy? { suspend fun fetch(uid: Int): SuPolicy? {
val query = "SELECT * FROM ${Table.POLICY} WHERE uid == $uid LIMIT = 1" val query = "SELECT * FROM ${Table.POLICY} WHERE uid == $uid LIMIT = 1"
return exec(query) { it.toPolicyOrNull() }.firstOrNull() return exec(query, ::toPolicy).firstOrNull()
} }
suspend fun update(policy: SuPolicy) { suspend fun update(policy: SuPolicy) {
val query = "REPLACE INTO ${Table.POLICY} ${policy.toMap().toQuery()}" val map = policy.toMap()
// Put in package_name for old database
map["package_name"] = AppContext.packageManager.getNameForUid(policy.uid)!!
val query = "REPLACE INTO ${Table.POLICY} ${map.toQuery()}"
exec(query) exec(query)
} }
suspend fun fetchAll(): List<SuPolicy> { suspend fun fetchAll(): List<SuPolicy> {
val query = "SELECT * FROM ${Table.POLICY} WHERE uid/100000 == ${Const.USER_ID}" val query = "SELECT * FROM ${Table.POLICY} WHERE uid/100000 == ${Const.USER_ID}"
return exec(query) { it.toPolicyOrNull() }.filterNotNull() return exec(query, ::toPolicy).filterNotNull()
} }
private suspend fun Map<String, String>.toPolicyOrNull(): SuPolicy? { private fun toPolicy(map: Map<String, String>): SuPolicy? {
try { val uid = map["uid"]?.toInt() ?: return null
return AppContext.packageManager.createPolicy(this) val policy = SuPolicy(uid)
} catch (e: Exception) {
Timber.w(e) map["policy"]?.toInt()?.let { policy.policy = it }
val uid = get("uid") ?: return null map["until"]?.toLong()?.let { policy.until = it }
delete(uid.toInt()) map["logging"]?.toInt()?.let { policy.logging = it != 0 }
return null map["notification"]?.toInt()?.let { policy.notification = it != 0 }
} return policy
} }
} }

View File

@ -1,75 +1,22 @@
package com.topjohnwu.magisk.core.model.su package com.topjohnwu.magisk.core.model.su
import android.content.pm.PackageInfo class SuPolicy(val uid: Int) {
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.ktx.getLabel
import com.topjohnwu.magisk.ktx.getPackageInfo
class SuPolicy(
val uid: Int,
val packageName: String,
val appName: String,
val icon: Drawable,
var policy: Int = INTERACTIVE,
var until: Long = -1L,
var logging: Boolean = true,
var notification: Boolean = true
) {
companion object { companion object {
const val INTERACTIVE = 0 const val INTERACTIVE = 0
const val DENY = 1 const val DENY = 1
const val ALLOW = 2 const val ALLOW = 2
} }
fun toMap() = mapOf( var policy: Int = INTERACTIVE
var until: Long = -1L
var logging: Boolean = true
var notification: Boolean = true
fun toMap(): MutableMap<String, Any> = mutableMapOf(
"uid" to uid, "uid" to uid,
"package_name" to packageName,
"policy" to policy, "policy" to policy,
"until" to until, "until" to until,
"logging" to logging, "logging" to logging,
"notification" to notification "notification" to notification
) )
} }
fun PackageManager.createPolicy(info: PackageInfo): SuPolicy {
val appInfo = info.applicationInfo
val prefix = if (info.sharedUserId == null) "" else "[SharedUID] "
return SuPolicy(
uid = appInfo.uid,
packageName = getNameForUid(appInfo.uid)!!,
appName = "$prefix${appInfo.getLabel(this)}",
icon = appInfo.loadIcon(this),
)
}
@Throws(PackageManager.NameNotFoundException::class)
fun PackageManager.createPolicy(uid: Int): SuPolicy {
val info = getPackageInfo(uid, -1)
return if (info == null) {
// We can assert getNameForUid does not return null because
// getPackageInfo will already throw if UID does not exist
val name = getNameForUid(uid)!!
SuPolicy(
uid = uid,
packageName = name,
appName = "[SharedUID] $name",
icon = defaultActivityIcon,
)
} else {
createPolicy(info)
}
}
@Throws(PackageManager.NameNotFoundException::class)
fun PackageManager.createPolicy(map: Map<String, String>): SuPolicy {
val uid = map["uid"]?.toIntOrNull() ?: throw IllegalArgumentException()
val policy = createPolicy(uid)
map["policy"]?.toInt()?.let { policy.policy = it }
map["until"]?.toLong()?.let { policy.until = it }
map["logging"]?.toInt()?.let { policy.logging = it != 0 }
map["notification"]?.toInt()?.let { policy.notification = it != 0 }
return policy
}

View File

@ -1,12 +1,12 @@
package com.topjohnwu.magisk.core.su package com.topjohnwu.magisk.core.su
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.magiskdb.PolicyDao import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.model.su.SuPolicy import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.core.model.su.createPolicy
import com.topjohnwu.magisk.ktx.getPackageInfo import com.topjohnwu.magisk.ktx.getPackageInfo
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -19,13 +19,15 @@ import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class SuRequestHandler( class SuRequestHandler(
private val pm: PackageManager, val pm: PackageManager,
private val policyDB: PolicyDao private val policyDB: PolicyDao
) : Closeable { ) : Closeable {
private lateinit var output: DataOutputStream private lateinit var output: DataOutputStream
lateinit var policy: SuPolicy lateinit var policy: SuPolicy
private set private set
lateinit var pkgInfo: PackageInfo
private set
// Return true to indicate undetermined policy, require user interaction // Return true to indicate undetermined policy, require user interaction
suspend fun start(intent: Intent): Boolean { suspend fun start(intent: Intent): Boolean {
@ -33,7 +35,7 @@ class SuRequestHandler(
return false return false
// Never allow com.topjohnwu.magisk (could be malware) // Never allow com.topjohnwu.magisk (could be malware)
if (policy.packageName == BuildConfig.APPLICATION_ID) { if (pkgInfo.packageName == BuildConfig.APPLICATION_ID) {
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec() Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
return false return false
} }
@ -64,9 +66,9 @@ class SuRequestHandler(
val fifo = intent.getStringExtra("fifo") ?: throw SuRequestError() val fifo = intent.getStringExtra("fifo") ?: throw SuRequestError()
val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() } val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
val pid = intent.getIntExtra("pid", -1) val pid = intent.getIntExtra("pid", -1)
val info = pm.getPackageInfo(uid, pid) ?: throw SuRequestError() pkgInfo = pm.getPackageInfo(uid, pid) ?: throw SuRequestError()
output = DataOutputStream(FileOutputStream(fifo).buffered()) output = DataOutputStream(FileOutputStream(fifo).buffered())
policy = pm.createPolicy(info) policy = SuPolicy(uid)
true true
} catch (e: Exception) { } catch (e: Exception) {
when (e) { when (e) {

View File

@ -10,37 +10,49 @@ import com.topjohnwu.magisk.databinding.RvContainer
import com.topjohnwu.magisk.databinding.set import com.topjohnwu.magisk.databinding.set
class PolicyRvItem( class PolicyRvItem(
private val viewModel: SuperuserViewModel,
override val item: SuPolicy, override val item: SuPolicy,
val packageName: String,
private val isSharedUid: Boolean,
val icon: Drawable, val icon: Drawable,
val viewModel: SuperuserViewModel val appName: String
) : ObservableDiffRvItem<PolicyRvItem>(), RvContainer<SuPolicy> { ) : ObservableDiffRvItem<PolicyRvItem>(), RvContainer<SuPolicy> {
override val layoutRes = R.layout.item_policy_md2 override val layoutRes = R.layout.item_policy_md2
val title get() = if (isSharedUid) "[SharedUID] $appName" else appName
private inline fun <reified T> setImpl(new: T, old: T, setter: (T) -> Unit) {
if (old != new) {
setter(new)
}
}
@get:Bindable @get:Bindable
var isExpanded = false var isExpanded = false
set(value) = set(value, field, { field = it }, BR.expanded) set(value) = set(value, field, { field = it }, BR.expanded)
// This property binds with the UI state
@get:Bindable @get:Bindable
var isEnabled var isEnabled
get() = item.policy == SuPolicy.ALLOW get() = item.policy == SuPolicy.ALLOW
set(value) { set(value) = setImpl(value, isEnabled) {
if (value != isEnabled) viewModel.togglePolicy(this, value)
viewModel.togglePolicy(this, value)
} }
@get:Bindable @get:Bindable
var shouldNotify var shouldNotify
get() = item.notification get() = item.notification
set(value) = set(value, shouldNotify, { item.notification = it }, BR.shouldNotify) { private set(value) = setImpl(value, shouldNotify) {
viewModel.updatePolicy(item, isLogging = false) item.notification = it
viewModel.updateNotify(this)
} }
@get:Bindable @get:Bindable
var shouldLog var shouldLog
get() = item.logging get() = item.logging
set(value) = set(value, shouldLog, { item.logging = it }, BR.shouldLog) { private set(value) = setImpl(value, shouldLog) {
viewModel.updatePolicy(item, isLogging = true) item.logging = it
viewModel.updateLogging(this)
} }
fun toggleExpand() { fun toggleExpand() {

View File

@ -1,5 +1,8 @@
package com.topjohnwu.magisk.ui.superuser package com.topjohnwu.magisk.ui.superuser
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import androidx.databinding.ObservableArrayList import androidx.databinding.ObservableArrayList
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
@ -12,9 +15,11 @@ import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.databinding.AnyDiffRvItem import com.topjohnwu.magisk.databinding.AnyDiffRvItem
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.di.AppContext
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.events.dialog.BiometricEvent import com.topjohnwu.magisk.events.dialog.BiometricEvent
import com.topjohnwu.magisk.events.dialog.SuperuserRevokeDialog import com.topjohnwu.magisk.events.dialog.SuperuserRevokeDialog
import com.topjohnwu.magisk.ktx.getLabel
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asText import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.TextItem import com.topjohnwu.magisk.view.TextItem
@ -41,6 +46,7 @@ class SuperuserViewModel(
// --- // ---
@SuppressLint("InlinedApi")
override fun refresh() = viewModelScope.launch { override fun refresh() = viewModelScope.launch {
if (!Utils.showSuperUser()) { if (!Utils.showSuperUser()) {
state = State.LOADING_FAILED state = State.LOADING_FAILED
@ -49,11 +55,28 @@ class SuperuserViewModel(
state = State.LOADING state = State.LOADING
val (policies, diff) = withContext(Dispatchers.IO) { val (policies, diff) = withContext(Dispatchers.IO) {
db.deleteOutdated() db.deleteOutdated()
val policies = db.fetchAll().map { val policies = ArrayList<PolicyRvItem>()
PolicyRvItem(it, it.icon, this@SuperuserViewModel) val pm = AppContext.packageManager
}.sortedWith(compareBy( for (policy in db.fetchAll()) {
{ it.item.appName.lowercase(currentLocale) }, val pkgs = pm.getPackagesForUid(policy.uid) ?: continue
{ it.item.packageName } policies.addAll(pkgs.mapNotNull { pkg ->
try {
val info = pm.getPackageInfo(pkg, MATCH_UNINSTALLED_PACKAGES)
PolicyRvItem(
this@SuperuserViewModel, policy,
info.packageName,
info.sharedUserId != null,
info.applicationInfo.loadIcon(pm),
info.applicationInfo.getLabel(pm)
)
} catch (e: PackageManager.NameNotFoundException) {
null
}
})
}
policies.sortWith(compareBy(
{ it.appName.lowercase(currentLocale) },
{ it.packageName }
)) ))
policies to itemsPolicies.calculateDiff(policies) policies to itemsPolicies.calculateDiff(policies)
} }
@ -82,40 +105,56 @@ class SuperuserViewModel(
}.publish() }.publish()
} else { } else {
SuperuserRevokeDialog { SuperuserRevokeDialog {
appName = item.item.appName appName = item.title
onSuccess { updateState() } onSuccess { updateState() }
}.publish() }.publish()
} }
} }
//--- fun updateNotify(item: PolicyRvItem) {
viewModelScope.launch {
fun updatePolicy(policy: SuPolicy, isLogging: Boolean) = viewModelScope.launch { db.update(item.item)
db.update(policy) val res = when {
val res = when { item.item.logging -> R.string.su_snack_log_on
isLogging -> when {
policy.logging -> R.string.su_snack_log_on
else -> R.string.su_snack_log_off else -> R.string.su_snack_log_off
} }
else -> when { itemsPolicies.forEach {
policy.notification -> R.string.su_snack_notif_on if (it.item.uid == item.item.uid) {
it.notifyPropertyChanged(BR.shouldNotify)
}
}
SnackbarEvent(res.asText(item.appName)).publish()
}
}
fun updateLogging(item: PolicyRvItem) {
viewModelScope.launch {
db.update(item.item)
val res = when {
item.item.notification -> R.string.su_snack_notif_on
else -> R.string.su_snack_notif_off else -> R.string.su_snack_notif_off
} }
itemsPolicies.forEach {
if (it.item.uid == item.item.uid) {
it.notifyPropertyChanged(BR.shouldLog)
}
}
SnackbarEvent(res.asText(item.appName)).publish()
} }
SnackbarEvent(res.asText(policy.appName)).publish()
} }
fun togglePolicy(item: PolicyRvItem, enable: Boolean) { fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
fun updateState() { fun updateState() {
val policy = if (enable) SuPolicy.ALLOW else SuPolicy.DENY
item.item.policy = policy
item.notifyPropertyChanged(BR.enabled)
viewModelScope.launch { viewModelScope.launch {
val res = if (enable) R.string.su_snack_grant else R.string.su_snack_deny
item.item.policy = if (enable) SuPolicy.ALLOW else SuPolicy.DENY
db.update(item.item) db.update(item.item)
val res = if (item.item.policy == SuPolicy.ALLOW) R.string.su_snack_grant itemsPolicies.forEach {
else R.string.su_snack_deny if (it.item.uid == item.item.uid) {
SnackbarEvent(res.asText(item.item.appName)).publish() it.notifyPropertyChanged(BR.enabled)
}
}
SnackbarEvent(res.asText(item.appName)).publish()
} }
} }

View File

@ -21,7 +21,6 @@ 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.Config
import com.topjohnwu.magisk.core.magiskdb.PolicyDao import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.DENY import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.DENY
import com.topjohnwu.magisk.core.su.SuRequestHandler import com.topjohnwu.magisk.core.su.SuRequestHandler
@ -31,6 +30,7 @@ import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.events.DieEvent import com.topjohnwu.magisk.events.DieEvent
import com.topjohnwu.magisk.events.ShowUIEvent import com.topjohnwu.magisk.events.ShowUIEvent
import com.topjohnwu.magisk.events.dialog.BiometricEvent import com.topjohnwu.magisk.events.dialog.BiometricEvent
import com.topjohnwu.magisk.ktx.getLabel
import com.topjohnwu.magisk.utils.TextHolder import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -100,17 +100,21 @@ class SuRequestViewModel(
fun handleRequest(intent: Intent) { fun handleRequest(intent: Intent) {
viewModelScope.launch { viewModelScope.launch {
if (handler.start(intent)) if (handler.start(intent))
showDialog(handler.policy) showDialog()
else else
DieEvent().publish() DieEvent().publish()
} }
} }
private fun showDialog(policy: SuPolicy) { private fun showDialog() {
icon = policy.icon val pm = handler.pm
title = policy.appName val info = handler.pkgInfo
packageName = policy.packageName val prefix = if (info.sharedUserId == null) "" else "[SharedUID] "
selectedItemPosition = timeoutPrefs.getInt(policy.packageName, 0)
icon = info.applicationInfo.loadIcon(pm)
title = "$prefix${info.applicationInfo.getLabel(pm)}"
packageName = info.packageName
selectedItemPosition = timeoutPrefs.getInt(packageName, 0)
// Set timer // Set timer
val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong()) val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
@ -124,7 +128,7 @@ class SuRequestViewModel(
timer?.cancel() timer?.cancel()
val pos = selectedItemPosition val pos = selectedItemPosition
timeoutPrefs.edit().putInt(handler.policy.packageName, pos).apply() timeoutPrefs.edit().putInt(handler.pkgInfo.packageName, pos).apply()
viewModelScope.launch { viewModelScope.launch {
handler.respond(action, Config.Value.TIMEOUT_LIST[pos]) handler.respond(action, Config.Value.TIMEOUT_LIST[pos])

View File

@ -54,7 +54,7 @@
android:ellipsize="middle" android:ellipsize="middle"
android:gravity="start" android:gravity="start"
android:maxLines="2" android:maxLines="2"
android:text="@{item.item.appName}" android:text="@{item.title}"
android:textAppearance="@style/AppearanceFoundation.Body" android:textAppearance="@style/AppearanceFoundation.Body"
android:textIsSelectable="false" android:textIsSelectable="false"
android:textStyle="bold" android:textStyle="bold"
@ -71,7 +71,7 @@
android:ellipsize="middle" android:ellipsize="middle"
android:gravity="start" android:gravity="start"
android:maxLines="2" android:maxLines="2"
android:text="@{item.item.packageName}" android:text="@{item.packageName}"
android:textAppearance="@style/AppearanceFoundation.Caption.Variant" android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
android:textColor="@android:color/tertiary_text_dark" android:textColor="@android:color/tertiary_text_dark"
android:textIsSelectable="false" android:textIsSelectable="false"