Add preliminary shared UID app support

This commit is contained in:
topjohnwu 2022-03-25 13:08:13 -07:00
parent f2c15c7701
commit 9f1740cc4f
19 changed files with 337 additions and 461 deletions

View File

@ -0,0 +1,8 @@
// IRootUtils.aidl
package com.topjohnwu.magisk.core.utils;
// Declare any non-default types here with import statements
interface IRootUtils {
android.app.ActivityManager.RunningAppProcessInfo getAppProcess(int pid);
}

View File

@ -1,28 +0,0 @@
package com.topjohnwu.magisk.core.magiskdb
import androidx.annotation.StringDef
abstract class BaseDao {
object Table {
const val POLICY = "policies"
const val LOG = "logs"
const val SETTINGS = "settings"
const val STRINGS = "strings"
}
@StringDef(Table.POLICY, Table.LOG, Table.SETTINGS, Table.STRINGS)
@Retention(AnnotationRetention.SOURCE)
annotation class TableStrict
@TableStrict
abstract val table: String
inline fun <reified Builder : Query.Builder> buildQuery(builder: Builder.() -> Unit = {}) =
Builder::class.java.newInstance()
.apply { table = this@BaseDao.table }
.apply(builder)
.toString()
.let { Query(it) }
}

View File

@ -0,0 +1,47 @@
package com.topjohnwu.magisk.core.magiskdb
import com.topjohnwu.magisk.ktx.await
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
open class MagiskDB {
suspend fun <R> exec(
query: String,
mapper: suspend (Map<String, String>) -> R
): List<R> {
return withContext(Dispatchers.IO) {
val out = Shell.cmd("magisk --sqlite '$query'").await().out
out.map { line ->
line.split("\\|".toRegex())
.map { it.split("=", limit = 2) }
.filter { it.size == 2 }
.associate { it[0] to it[1] }
.let { mapper(it) }
}
}
}
suspend inline fun exec(query: String) {
exec(query) {}
}
fun Map<String, Any>.toQuery(): String {
val keys = this.keys.joinToString(",")
val values = this.values.joinToString(",") {
when (it) {
is Boolean -> if (it) "1" else "0"
is Number -> it.toString()
else -> "\"$it\""
}
}
return "($keys) VALUES($values)"
}
object Table {
const val POLICY = "policies"
const val SETTINGS = "settings"
const val STRINGS = "strings"
}
}

View File

@ -2,61 +2,48 @@ 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.toPolicy
import com.topjohnwu.magisk.core.model.su.createPolicy
import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.ktx.now
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.TimeUnit
class PolicyDao : MagiskDB() {
class PolicyDao : BaseDao() {
override val table: String = Table.POLICY
suspend fun deleteOutdated() = buildQuery<Delete> {
condition {
greaterThan("until", "0")
and {
lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString())
}
or {
lessThan("until", "0")
}
}
}.commit()
suspend fun delete(uid: Int) = buildQuery<Delete> {
condition {
equals("uid", uid)
}
}.commit()
suspend fun fetch(uid: Int) = buildQuery<Select> {
condition {
equals("uid", uid)
}
}.query().first().toPolicyOrNull()
suspend fun update(policy: SuPolicy) = buildQuery<Replace> {
values(policy.toMap())
}.commit()
suspend fun <R: Any> fetchAll(mapper: (SuPolicy) -> R) = buildQuery<Select> {
condition {
equals("uid/100000", Const.USER_ID)
}
}.query {
it.toPolicyOrNull()?.let(mapper)
suspend fun deleteOutdated() {
val nowSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
val query = "DELETE FROM ${Table.POLICY} WHERE " +
"(until > 0 AND until < $nowSeconds) OR until < 0"
exec(query)
}
private fun Map<String, String>.toPolicyOrNull(): SuPolicy? {
return runCatching { toPolicy(AppContext.packageManager) }.getOrElse {
Timber.w(it)
val uid = getOrElse("uid") { return null }
GlobalScope.launch { delete(uid.toInt()) }
null
suspend fun delete(uid: Int) {
val query = "DELETE FROM ${Table.POLICY} WHERE uid == $uid"
exec(query)
}
suspend fun fetch(uid: Int): SuPolicy? {
val query = "SELECT * FROM ${Table.POLICY} WHERE uid == $uid LIMIT = 1"
return exec(query) { it.toPolicyOrNull() }.firstOrNull()
}
suspend fun update(policy: SuPolicy) {
val query = "REPLACE INTO ${Table.POLICY} ${policy.toMap().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()
}
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
}
}

View File

@ -1,161 +0,0 @@
package com.topjohnwu.magisk.core.magiskdb
import androidx.annotation.StringDef
import com.topjohnwu.magisk.ktx.await
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
class Query(private val _query: String) {
val query get() = "magisk --sqlite '$_query'"
interface Builder {
val requestType: String
var table: String
}
suspend inline fun <R : Any> query(crossinline mapper: (Map<String, String>) -> R?): List<R> =
withContext(Dispatchers.Default) {
Shell.cmd(query).await().out.map { line ->
async {
line.split("\\|".toRegex())
.map { it.split("=", limit = 2) }
.filter { it.size == 2 }
.map { it[0] to it[1] }
.toMap()
.let(mapper)
}
}.awaitAll().filterNotNull()
}
suspend inline fun query() = query { it }
suspend inline fun commit() = Shell.cmd(query).to(null).await()
}
class Delete : Query.Builder {
override val requestType: String = "DELETE FROM"
override var table = ""
private var condition = ""
fun condition(builder: Condition.() -> Unit) {
condition = Condition().apply(builder).toString()
}
override fun toString(): String {
return listOf(requestType, table, condition).joinToString(" ")
}
}
class Select : Query.Builder {
override val requestType: String get() = "SELECT $fields FROM"
override lateinit var table: String
private var fields = "*"
private var condition = ""
private var orderField = ""
fun fields(vararg newFields: String) {
if (newFields.isEmpty()) {
fields = "*"
return
}
fields = newFields.joinToString(", ")
}
fun condition(builder: Condition.() -> Unit) {
condition = Condition().apply(builder).toString()
}
fun orderBy(field: String, @OrderStrict order: String) {
orderField = "ORDER BY $field $order"
}
override fun toString(): String {
return listOf(requestType, table, condition, orderField).joinToString(" ")
}
}
class Replace : Insert() {
override val requestType: String = "REPLACE INTO"
}
open class Insert : Query.Builder {
override val requestType: String = "INSERT INTO"
override lateinit var table: String
private val keys get() = _values.keys.joinToString(",")
private val values get() = _values.values.joinToString(",") {
when (it) {
is Boolean -> if (it) "1" else "0"
is Number -> it.toString()
else -> "\"$it\""
}
}
private var _values: Map<String, Any> = mapOf()
fun values(vararg pairs: Pair<String, Any>) {
_values = pairs.toMap()
}
fun values(values: Map<String, Any>) {
_values = values
}
override fun toString(): String {
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
}
}
class Condition {
private val conditionWord = "WHERE %s"
private var condition: String = ""
fun equals(field: String, value: Any) {
condition = when (value) {
is String -> "$field=\"$value\""
else -> "$field=$value"
}
}
fun greaterThan(field: String, value: String) {
condition = "$field > $value"
}
fun lessThan(field: String, value: String) {
condition = "$field < $value"
}
fun greaterOrEqualTo(field: String, value: String) {
condition = "$field >= $value"
}
fun lessOrEqualTo(field: String, value: String) {
condition = "$field <= $value"
}
fun and(builder: Condition.() -> Unit) {
condition = "($condition AND ${Condition().apply(builder).condition})"
}
fun or(builder: Condition.() -> Unit) {
condition = "($condition OR ${Condition().apply(builder).condition})"
}
override fun toString(): String {
return conditionWord.format(condition)
}
}
object Order {
const val ASC = "ASC"
const val DESC = "DESC"
}
@StringDef(Order.ASC, Order.DESC)
@Retention(AnnotationRetention.SOURCE)
annotation class OrderStrict

View File

@ -1,22 +1,20 @@
package com.topjohnwu.magisk.core.magiskdb
class SettingsDao : BaseDao() {
class SettingsDao : MagiskDB() {
override val table = Table.SETTINGS
suspend fun delete(key: String) {
val query = "DELETE FROM ${Table.SETTINGS} WHERE key == \"$key\""
exec(query)
}
suspend fun delete(key: String) = buildQuery<Delete> {
condition { equals("key", key) }
}.commit()
suspend fun put(key: String, value: Int) = buildQuery<Replace> {
values("key" to key, "value" to value)
}.commit()
suspend fun fetch(key: String, default: Int = -1) = buildQuery<Select> {
fields("value")
condition { equals("key", key) }
}.query {
it["value"]?.toIntOrNull()
}.firstOrNull() ?: default
suspend fun put(key: String, value: Int) {
val kv = mapOf("key" to key, "value" to value)
val query = "REPLACE INTO ${Table.SETTINGS} ${kv.toQuery()}"
exec(query)
}
suspend fun fetch(key: String, default: Int = -1): Int {
val query = "SELECT value FROM ${Table.SETTINGS} WHERE key == \"$key\" LIMIT = 1"
return exec(query) { it["value"]?.toInt() }.firstOrNull() ?: default
}
}

View File

@ -1,22 +1,20 @@
package com.topjohnwu.magisk.core.magiskdb
class StringDao : BaseDao() {
class StringDao : MagiskDB() {
override val table = Table.STRINGS
suspend fun delete(key: String) {
val query = "DELETE FROM ${Table.STRINGS} WHERE key == \"$key\""
exec(query)
}
suspend fun delete(key: String) = buildQuery<Delete> {
condition { equals("key", key) }
}.commit()
suspend fun put(key: String, value: String) = buildQuery<Replace> {
values("key" to key, "value" to value)
}.commit()
suspend fun fetch(key: String, default: String = "") = buildQuery<Select> {
fields("value")
condition { equals("key", key) }
}.query {
it["value"]
}.firstOrNull() ?: default
suspend fun put(key: String, value: String) {
val kv = mapOf("key" to key, "value" to value)
val query = "REPLACE INTO ${Table.STRINGS} ${kv.toQuery()}"
exec(query)
}
suspend fun fetch(key: String, default: String = ""): String {
val query = "SELECT value FROM ${Table.STRINGS} WHERE key == \"$key\" LIMIT = 1"
return exec(query) { it["value"] }.firstOrNull() ?: default
}
}

View File

@ -1,11 +1,13 @@
package com.topjohnwu.magisk.core.model.su
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.ktx.now
import com.topjohnwu.magisk.ktx.getLabel
@Entity(tableName = "logs")
data class SuLog(
class SuLog(
val fromUid: Int,
val toUid: Int,
val fromPid: Int,
@ -13,7 +15,44 @@ data class SuLog(
val appName: String,
val command: String,
val action: Boolean,
val time: Long = now
val time: Long = System.currentTimeMillis()
) {
@PrimaryKey(autoGenerate = true) var id: Int = 0
}
fun PackageManager.createSuLog(
info: PackageInfo,
toUid: Int,
fromPid: Int,
command: String,
policy: Int
): SuLog {
val appInfo = info.applicationInfo
return SuLog(
fromUid = appInfo.uid,
toUid = toUid,
fromPid = fromPid,
packageName = getNameForUid(appInfo.uid)!!,
appName = appInfo.getLabel(this),
command = command,
action = policy == SuPolicy.ALLOW
)
}
fun createSuLog(
fromUid: Int,
toUid: Int,
fromPid: Int,
command: String,
policy: Int
): SuLog {
return SuLog(
fromUid = fromUid,
toUid = toUid,
fromPid = fromPid,
packageName = "[UID] $fromUid",
appName = "[UID] $fromUid",
command = command,
action = policy == SuPolicy.ALLOW
)
}

View File

@ -1,22 +1,20 @@
@file:SuppressLint("InlinedApi")
package com.topjohnwu.magisk.core.model.su
import android.annotation.SuppressLint
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
import com.topjohnwu.magisk.ktx.getLabel
import com.topjohnwu.magisk.ktx.getPackageInfo
data class SuPolicy(
class SuPolicy(
val uid: Int,
val packageName: String,
val appName: String,
val icon: Drawable,
var policy: Int = INTERACTIVE,
var until: Long = -1L,
val logging: Boolean = true,
val notification: Boolean = true
var logging: Boolean = true,
var notification: Boolean = true
) {
companion object {
@ -25,10 +23,6 @@ data class SuPolicy(
const val ALLOW = 2
}
fun toLog(toUid: Int, fromPid: Int, command: String) = SuLog(
uid, toUid, fromPid, packageName, appName,
command, policy == ALLOW)
fun toMap() = mapOf(
"uid" to uid,
"package_name" to packageName,
@ -39,47 +33,43 @@ data class SuPolicy(
)
}
@Throws(PackageManager.NameNotFoundException::class)
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
val uid = get("uid")?.toIntOrNull() ?: -1
val packageName = get("package_name").orEmpty()
val info = pm.getApplicationInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES)
if (info.uid != uid)
throw PackageManager.NameNotFoundException()
fun PackageManager.createPolicy(info: PackageInfo): SuPolicy {
val appInfo = info.applicationInfo
val prefix = if (info.sharedUserId == null) "" else "[SharedUID] "
return SuPolicy(
uid = uid,
packageName = packageName,
appName = info.getLabel(pm),
icon = info.loadIcon(pm),
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
until = get("until")?.toLongOrNull() ?: -1L,
logging = get("logging")?.toIntOrNull() != 0,
notification = get("notification")?.toIntOrNull() != 0
uid = appInfo.uid,
packageName = getNameForUid(appInfo.uid)!!,
appName = "$prefix${appInfo.getLabel(this)}",
icon = appInfo.loadIcon(this),
)
}
@Throws(PackageManager.NameNotFoundException::class)
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
?: throw PackageManager.NameNotFoundException()
val info = pm.getApplicationInfo(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES)
return SuPolicy(
uid = info.uid,
packageName = pkg,
appName = info.getLabel(pm),
icon = info.loadIcon(pm),
policy = policy
)
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)
}
}
fun Int.toUidPolicy(pm: PackageManager, policy: Int): SuPolicy {
return SuPolicy(
uid = this,
packageName = "[UID] $this",
appName = "[UID] $this",
icon = pm.defaultActivityIcon,
policy = policy
)
@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

@ -7,12 +7,12 @@ import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.core.model.su.toPolicy
import com.topjohnwu.magisk.core.model.su.toUidPolicy
import com.topjohnwu.magisk.core.model.su.createSuLog
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.getLabel
import com.topjohnwu.magisk.ktx.getPackageInfo
import com.topjohnwu.magisk.utils.Utils
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber
object SuCallbackHandler {
@ -53,59 +53,47 @@ object SuCallbackHandler {
private fun handleLogging(context: Context, data: Bundle) {
val fromUid = data.getIntComp("from.uid", -1)
val notify = data.getBoolean("notify", true)
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
val toUid = data.getIntComp("to.uid", -1)
val pid = data.getIntComp("pid", -1)
val command = data.getString("command", "")
val pm = context.packageManager
val policy = runCatching {
fromUid.toPolicy(pm, allow)
}.getOrElse {
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
fromUid.toUidPolicy(pm, allow)
}
val log = runCatching {
pm.getPackageInfo(fromUid, pid)?.let {
pm.createSuLog(it, toUid, pid, command, policy)
}
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy)
if (notify)
notify(context, policy)
notify(context, log.action, log.appName)
val toUid = data.getIntComp("to.uid", -1)
val pid = data.getIntComp("pid", -1)
val command = data.getString("command", "")
val log = policy.toLog(
toUid = toUid,
fromPid = pid,
command = command
)
GlobalScope.launch {
ServiceLocator.logRepo.insert(log)
}
runBlocking { ServiceLocator.logRepo.insert(log) }
}
private fun handleNotify(context: Context, data: Bundle) {
val fromUid = data.getIntComp("from.uid", -1)
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
val uid = data.getIntComp("from.uid", -1)
val pid = data.getIntComp("pid", -1)
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
val pm = context.packageManager
val appName = runCatching {
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
}.getOrNull() ?: "[UID] $uid"
val policy = runCatching {
fromUid.toPolicy(pm, allow)
}.getOrElse {
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
fromUid.toUidPolicy(pm, allow)
}
notify(context, policy)
notify(context, policy == SuPolicy.ALLOW, appName)
}
private fun notify(context: Context, policy: SuPolicy) {
private fun notify(context: Context, granted: Boolean, appName: String) {
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
val resId = if (policy.policy == SuPolicy.ALLOW)
val resId = if (granted)
R.string.su_allow_toast
else
R.string.su_deny_toast
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
Utils.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
}
}
}

View File

@ -6,7 +6,8 @@ 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.toPolicy
import com.topjohnwu.magisk.core.model.su.createPolicy
import com.topjohnwu.magisk.ktx.getPackageInfo
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -60,10 +61,12 @@ class SuRequestHandler(
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
try {
val name = intent.getStringExtra("fifo") ?: throw SuRequestError()
val fifo = intent.getStringExtra("fifo") ?: throw SuRequestError()
val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
output = DataOutputStream(FileOutputStream(name).buffered())
policy = uid.toPolicy(pm)
val pid = intent.getIntExtra("pid", -1)
val info = pm.getPackageInfo(uid, pid) ?: throw SuRequestError()
output = DataOutputStream(FileOutputStream(fifo).buffered())
policy = pm.createPolicy(info)
true
} catch (e: Exception) {
when (e) {

View File

@ -1,30 +1,64 @@
package com.topjohnwu.magisk.core.utils
import android.app.ActivityManager
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Binder
import android.os.IBinder
import androidx.core.content.getSystemService
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.ipc.RootService
import timber.log.Timber
import java.io.File
import java.util.concurrent.locks.AbstractQueuedSynchronizer
class RootUtils(stub: Any?) : RootService() {
private val className: String = stub?.javaClass?.name ?: javaClass.name
private lateinit var am: ActivityManager
constructor() : this(null) {
Timber.plant(Timber.DebugTree())
}
override fun onCreate() {
am = getSystemService()!!
}
override fun getComponentName(): ComponentName {
return ComponentName(packageName, className)
}
override fun onBind(intent: Intent): IBinder {
return Binder()
return object : IRootUtils.Stub() {
override fun getAppProcess(pid: Int) = safe(null) { getAppProcessImpl(pid) }
}
}
private inline fun <T> safe(default: T, block: () -> T): T {
return try {
block()
} catch (e: Throwable) {
Timber.e(e)
default
}
}
private fun getAppProcessImpl(_pid: Int): ActivityManager.RunningAppProcessInfo? {
val procList = am.runningAppProcesses
var pid = _pid
while (pid > 1) {
val proc = procList.find { it.pid == pid }
if (proc != null)
return proc
// Find PPID
File("/proc/$pid/status").useLines {
val s = it.find { line -> line.startsWith("PPid:") }
pid = s?.substring(5)?.trim()?.toInt() ?: -1
}
}
return null
}
object Connection : AbstractQueuedSynchronizer(), ServiceConnection {

View File

@ -7,10 +7,10 @@ import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.*
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.content.res.Configuration
import android.content.res.Resources
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.Canvas
@ -19,40 +19,26 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.text.PrecomputedText
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.core.text.PrecomputedTextCompat
import androidx.core.view.isGone
import androidx.core.widget.TextViewCompat
import androidx.databinding.BindingAdapter
import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.utils.DynamicClassLoader
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
import kotlin.Array
import kotlin.String
import java.lang.reflect.Array as JArray
fun Context.rawResource(id: Int) = resources.openRawResource(id)
@ -79,8 +65,6 @@ val Context.deviceProtectedContext: Context get() =
createDeviceProtectedStorageContext()
} else { this }
fun Intent.startActivity(context: Context) = context.startActivity(this)
fun Intent.startActivityWithRoot() {
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
val cmd = toCommand(args).joinToString(" ")
@ -185,8 +169,6 @@ fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<S
return args
}
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
fun Context.cachedFile(name: String) = File(cacheDir, name)
fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
@ -209,34 +191,6 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String {
return loadLabel(pm).toString()
}
fun Intent.exists(packageManager: PackageManager) = resolveActivity(packageManager) != null
fun Context.colorCompat(@ColorRes id: Int) = try {
ContextCompat.getColor(this, id)
} catch (e: Resources.NotFoundException) {
null
}
fun Context.colorStateListCompat(@ColorRes id: Int) = try {
ContextCompat.getColorStateList(this, id)
} catch (e: Resources.NotFoundException) {
null
}
fun Context.drawableCompat(@DrawableRes id: Int) = AppCompatResources.getDrawable(this, id)
/**
* Pass [start] and [end] dimensions, function will return left and right
* with respect to RTL layout direction
*/
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
return end to start
}
return start to end
}
fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri())
inline fun <reified T> T.createClassLoader(apk: File) =
DynamicClassLoader(apk, T::class.java.classLoader)
@ -308,3 +262,20 @@ fun getProperty(key: String, def: String): String {
}
return def
}
@SuppressLint("InlinedApi")
@Throws(PackageManager.NameNotFoundException::class)
fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
val flag = PackageManager.MATCH_UNINSTALLED_PACKAGES
val pkgs = getPackagesForUid(uid) ?: throw PackageManager.NameNotFoundException()
return if (pkgs.size > 1) {
if (pid <= 0)
return null
// Try to find package name from PID
val proc = RootUtils.obj?.getAppProcess(pid) ?: return null
val pkg = proc.pkgList[0]
getPackageInfo(pkg, flag)
} else {
getPackageInfo(pkgs[0], flag)
}
}

View File

@ -20,34 +20,28 @@ class PolicyRvItem(
var isExpanded = false
set(value) = set(value, field, { field = it }, BR.expanded)
// This property hosts the policy state
var policyState = item.policy == SuPolicy.ALLOW
set(value) = set(value, field, { field = it }, BR.enabled)
// This property binds with the UI state
@get:Bindable
var isEnabled
get() = policyState
set(value) = set(value, policyState, { viewModel.togglePolicy(this, it) }, BR.enabled)
@get:Bindable
var shouldNotify = item.notification
set(value) = set(value, field, { field = it }, BR.shouldNotify) {
viewModel.updatePolicy(updatedPolicy, isLogging = false)
get() = item.policy == SuPolicy.ALLOW
set(value) {
if (value != isEnabled)
viewModel.togglePolicy(this, value)
}
@get:Bindable
var shouldLog = item.logging
set(value) = set(value, field, { field = it }, BR.shouldLog) {
viewModel.updatePolicy(updatedPolicy, isLogging = true)
var shouldNotify
get() = item.notification
set(value) = set(value, shouldNotify, { item.notification = it }, BR.shouldNotify) {
viewModel.updatePolicy(item, isLogging = false)
}
private val updatedPolicy
get() = item.copy(
policy = if (policyState) SuPolicy.ALLOW else SuPolicy.DENY,
notification = shouldNotify,
logging = shouldLog
)
@get:Bindable
var shouldLog
get() = item.logging
set(value) = set(value, shouldLog, { item.logging = it }, BR.shouldLog) {
viewModel.updatePolicy(item, isLogging = true)
}
fun toggleExpand() {
isExpanded = !isExpanded

View File

@ -47,12 +47,12 @@ class SuperuserViewModel(
return@launch
}
state = State.LOADING
val (policies, diff) = withContext(Dispatchers.Default) {
val (policies, diff) = withContext(Dispatchers.IO) {
db.deleteOutdated()
val policies = db.fetchAll {
val policies = db.fetchAll().map {
PolicyRvItem(it, it.icon, this@SuperuserViewModel)
}.sortedWith(compareBy(
{ it.item.appName.toLowerCase(currentLocale) },
{ it.item.appName.lowercase(currentLocale) },
{ it.item.packageName }
))
policies to itemsPolicies.calculateDiff(policies)
@ -107,14 +107,13 @@ class SuperuserViewModel(
fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
fun updateState() {
item.policyState = enable
val policy = if (enable) SuPolicy.ALLOW else SuPolicy.DENY
val app = item.item.copy(policy = policy)
item.item.policy = policy
item.notifyPropertyChanged(BR.enabled)
viewModelScope.launch {
db.update(app)
val res = if (app.policy == SuPolicy.ALLOW) R.string.su_snack_grant
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()
}

View File

@ -7,6 +7,7 @@ import android.os.Build
import android.os.Bundle
import android.view.Window
import android.view.WindowManager
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.core.su.SuCallbackHandler
@ -14,6 +15,9 @@ import com.topjohnwu.magisk.core.su.SuCallbackHandler.REQUEST
import com.topjohnwu.magisk.databinding.ActivityRequestBinding
import com.topjohnwu.magisk.di.viewModel
import com.topjohnwu.magisk.ui.theme.Theme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
open class SuRequestActivity : UIActivity<ActivityRequestBinding>() {
@ -40,8 +44,12 @@ open class SuRequestActivity : UIActivity<ActivityRequestBinding>() {
if (action == REQUEST) {
viewModel.handleRequest(intent)
} else {
SuCallbackHandler.run(this, action, intent.extras)
finish()
lifecycleScope.launch {
withContext(Dispatchers.IO) {
SuCallbackHandler.run(this@SuRequestActivity, action, intent.extras)
}
finish()
}
}
} else {
finish()

View File

@ -191,6 +191,7 @@ void app_notify(const su_context &ctx) {
vector<Extra> extras;
extras.reserve(2);
extras.emplace_back("from.uid", ctx.info->uid);
extras.emplace_back("pid", ctx.pid);
extras.emplace_back("policy", ctx.info->access.policy);
exec_cmd("notify", extras, ctx.info);
@ -198,21 +199,22 @@ void app_notify(const su_context &ctx) {
}
}
int app_request(const shared_ptr<su_info> &info) {
int app_request(const su_context &ctx) {
// Create FIFO
char fifo[64];
strcpy(fifo, "/dev/socket/");
gen_rand_str(fifo + 12, 32, true);
mkfifo(fifo, 0600);
chown(fifo, info->mgr_st.st_uid, info->mgr_st.st_gid);
chown(fifo, ctx.info->mgr_st.st_uid, ctx.info->mgr_st.st_gid);
setfilecon(fifo, "u:object_r:" SEPOL_FILE_TYPE ":s0");
// Send request
vector<Extra> extras;
extras.reserve(2);
extras.emplace_back("fifo", fifo);
extras.emplace_back("uid", info->eval_uid);
exec_cmd("request", extras, info, false);
extras.emplace_back("uid", ctx.info->eval_uid);
extras.emplace_back("pid", ctx.pid);
exec_cmd("request", extras, ctx.info, false);
// Wait for data input for at most 70 seconds
int fd = xopen(fifo, O_RDONLY | O_CLOEXEC | O_NONBLOCK);

View File

@ -60,4 +60,4 @@ struct su_context {
void app_log(const su_context &ctx);
void app_notify(const su_context &ctx);
int app_request(const std::shared_ptr<su_info> &info);
int app_request(const su_context &ctx);

View File

@ -193,20 +193,7 @@ static shared_ptr<su_info> get_su_info(unsigned uid) {
info->access = NO_SU_ACCESS;
return info;
}
} else {
return info;
}
// If still not determined, ask manager
int fd = app_request(info);
if (fd < 0) {
info->access.policy = DENY;
} else {
int ret = read_int_be(fd);
info->access.policy = ret < 0 ? DENY : static_cast<policy_t>(ret);
close(fd);
}
return info;
}
@ -237,6 +224,18 @@ void su_daemon_handler(int client, const sock_cred *cred) {
read_string(client, ctx.req.shell);
read_string(client, ctx.req.command);
// If still not determined, ask manager
if (ctx.info->access.policy == QUERY) {
int fd = app_request(ctx);
if (fd < 0) {
ctx.info->access.policy = DENY;
} else {
int ret = read_int_be(fd);
ctx.info->access.policy = ret < 0 ? DENY : static_cast<policy_t>(ret);
close(fd);
}
}
if (ctx.info->access.log)
app_log(ctx);
else if (ctx.info->access.notify)