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.model.su.SuPolicy
import com.topjohnwu.magisk.core.model.su.createPolicy
import com.topjohnwu.magisk.di.AppContext
import timber.log.Timber
import java.util.concurrent.TimeUnit
class PolicyDao : MagiskDB() {
@ -23,28 +21,31 @@ class PolicyDao : MagiskDB() {
suspend fun fetch(uid: Int): SuPolicy? {
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) {
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)
}
suspend fun fetchAll(): List<SuPolicy> {
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? {
try {
return AppContext.packageManager.createPolicy(this)
} catch (e: Exception) {
Timber.w(e)
val uid = get("uid") ?: return null
delete(uid.toInt())
return null
}
private fun toPolicy(map: Map<String, String>): SuPolicy? {
val uid = map["uid"]?.toInt() ?: return null
val policy = SuPolicy(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,75 +1,22 @@
package com.topjohnwu.magisk.core.model.su
import android.content.pm.PackageInfo
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
) {
class SuPolicy(val uid: Int) {
companion object {
const val INTERACTIVE = 0
const val DENY = 1
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,
"package_name" to packageName,
"policy" to policy,
"until" to until,
"logging" to logging,
"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
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
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.superuser.Shell
import kotlinx.coroutines.Dispatchers
@ -19,13 +19,15 @@ import java.io.IOException
import java.util.concurrent.TimeUnit
class SuRequestHandler(
private val pm: PackageManager,
val pm: PackageManager,
private val policyDB: PolicyDao
) : Closeable {
private lateinit var output: DataOutputStream
lateinit var policy: SuPolicy
private set
lateinit var pkgInfo: PackageInfo
private set
// Return true to indicate undetermined policy, require user interaction
suspend fun start(intent: Intent): Boolean {
@ -33,7 +35,7 @@ class SuRequestHandler(
return false
// 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()
return false
}
@ -64,9 +66,9 @@ class SuRequestHandler(
val fifo = intent.getStringExtra("fifo") ?: throw SuRequestError()
val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
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())
policy = pm.createPolicy(info)
policy = SuPolicy(uid)
true
} catch (e: Exception) {
when (e) {

View File

@ -10,37 +10,49 @@ import com.topjohnwu.magisk.databinding.RvContainer
import com.topjohnwu.magisk.databinding.set
class PolicyRvItem(
private val viewModel: SuperuserViewModel,
override val item: SuPolicy,
val packageName: String,
private val isSharedUid: Boolean,
val icon: Drawable,
val viewModel: SuperuserViewModel
val appName: String
) : ObservableDiffRvItem<PolicyRvItem>(), RvContainer<SuPolicy> {
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
var isExpanded = false
set(value) = set(value, field, { field = it }, BR.expanded)
// This property binds with the UI state
@get:Bindable
var isEnabled
get() = item.policy == SuPolicy.ALLOW
set(value) {
if (value != isEnabled)
viewModel.togglePolicy(this, value)
set(value) = setImpl(value, isEnabled) {
viewModel.togglePolicy(this, value)
}
@get:Bindable
var shouldNotify
get() = item.notification
set(value) = set(value, shouldNotify, { item.notification = it }, BR.shouldNotify) {
viewModel.updatePolicy(item, isLogging = false)
private set(value) = setImpl(value, shouldNotify) {
item.notification = it
viewModel.updateNotify(this)
}
@get:Bindable
var shouldLog
get() = item.logging
set(value) = set(value, shouldLog, { item.logging = it }, BR.shouldLog) {
viewModel.updatePolicy(item, isLogging = true)
private set(value) = setImpl(value, shouldLog) {
item.logging = it
viewModel.updateLogging(this)
}
fun toggleExpand() {

View File

@ -1,5 +1,8 @@
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.lifecycle.viewModelScope
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.diffListOf
import com.topjohnwu.magisk.databinding.itemBindingOf
import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.events.dialog.BiometricEvent
import com.topjohnwu.magisk.events.dialog.SuperuserRevokeDialog
import com.topjohnwu.magisk.ktx.getLabel
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.TextItem
@ -41,6 +46,7 @@ class SuperuserViewModel(
// ---
@SuppressLint("InlinedApi")
override fun refresh() = viewModelScope.launch {
if (!Utils.showSuperUser()) {
state = State.LOADING_FAILED
@ -49,11 +55,28 @@ class SuperuserViewModel(
state = State.LOADING
val (policies, diff) = withContext(Dispatchers.IO) {
db.deleteOutdated()
val policies = db.fetchAll().map {
PolicyRvItem(it, it.icon, this@SuperuserViewModel)
}.sortedWith(compareBy(
{ it.item.appName.lowercase(currentLocale) },
{ it.item.packageName }
val policies = ArrayList<PolicyRvItem>()
val pm = AppContext.packageManager
for (policy in db.fetchAll()) {
val pkgs = pm.getPackagesForUid(policy.uid) ?: continue
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)
}
@ -82,40 +105,56 @@ class SuperuserViewModel(
}.publish()
} else {
SuperuserRevokeDialog {
appName = item.item.appName
appName = item.title
onSuccess { updateState() }
}.publish()
}
}
//---
fun updatePolicy(policy: SuPolicy, isLogging: Boolean) = viewModelScope.launch {
db.update(policy)
val res = when {
isLogging -> when {
policy.logging -> R.string.su_snack_log_on
fun updateNotify(item: PolicyRvItem) {
viewModelScope.launch {
db.update(item.item)
val res = when {
item.item.logging -> R.string.su_snack_log_on
else -> R.string.su_snack_log_off
}
else -> when {
policy.notification -> R.string.su_snack_notif_on
itemsPolicies.forEach {
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
}
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 updateState() {
val policy = if (enable) SuPolicy.ALLOW else SuPolicy.DENY
item.item.policy = policy
item.notifyPropertyChanged(BR.enabled)
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)
val res = if (item.item.policy == SuPolicy.ALLOW) R.string.su_snack_grant
else R.string.su_snack_deny
SnackbarEvent(res.asText(item.item.appName)).publish()
itemsPolicies.forEach {
if (it.item.uid == item.item.uid) {
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.core.Config
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.DENY
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.ShowUIEvent
import com.topjohnwu.magisk.events.dialog.BiometricEvent
import com.topjohnwu.magisk.ktx.getLabel
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.Utils
import kotlinx.coroutines.launch
@ -100,17 +100,21 @@ class SuRequestViewModel(
fun handleRequest(intent: Intent) {
viewModelScope.launch {
if (handler.start(intent))
showDialog(handler.policy)
showDialog()
else
DieEvent().publish()
}
}
private fun showDialog(policy: SuPolicy) {
icon = policy.icon
title = policy.appName
packageName = policy.packageName
selectedItemPosition = timeoutPrefs.getInt(policy.packageName, 0)
private fun showDialog() {
val pm = handler.pm
val info = handler.pkgInfo
val prefix = if (info.sharedUserId == null) "" else "[SharedUID] "
icon = info.applicationInfo.loadIcon(pm)
title = "$prefix${info.applicationInfo.getLabel(pm)}"
packageName = info.packageName
selectedItemPosition = timeoutPrefs.getInt(packageName, 0)
// Set timer
val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
@ -124,7 +128,7 @@ class SuRequestViewModel(
timer?.cancel()
val pos = selectedItemPosition
timeoutPrefs.edit().putInt(handler.policy.packageName, pos).apply()
timeoutPrefs.edit().putInt(handler.pkgInfo.packageName, pos).apply()
viewModelScope.launch {
handler.respond(action, Config.Value.TIMEOUT_LIST[pos])

View File

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