mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-12-02 12:21:50 +00:00
Use ContentProvider call method for communication
Previously, we use either BroadcastReceivers or Activities to receive messages from our native daemon, but both have their own downsides. Some OEMs blocks broadcasts if the app is not running in the background, regardless of who the caller is. Activities on the other hand, despite working 100% of the time, will steal the focus of the current foreground app, even though we are just doing some logging and showing a toast. In addition, since stubs for hiding Magisk Manager is introduced, our only communication method is left with the broadcast option, as only broadcasting allows targeting a specific package name, not a component name (which will be obfuscated in the case of stubs). To make sure root requests will work on all devices, Magisk had to do some experiments every boot to test whether broadcast is deliverable or not. This makes the whole thing even more complicated then ever. So lets take a look at another kind of component in Android apps: ContentProviders. It is a vital part of Android's ecosystem, and as far as I know no OEMs will block requests to ContentProviders (or else tons of functionality will break catastrophically). Starting at API 11, the system supports calling a specific method in ContentProviders, optionally sending extra data along with the method call. This is perfect for the native daemon to start a communication with Magisk Manager. Another cool thing is that we no longer need to know the component name of the reciever, as ContentProviders identify themselves with an "authority" name, which in Magisk Manager's case is tied to the package name. We already have a mechanism to keep track of our current manager package name, so this works out of the box. So yay! No more flaky broadcast tests, no more stupid OEMs blocking broadcasts for some bizzare reasons. This method should in theory work on almost all devices and situations.
This commit is contained in:
@@ -16,6 +16,7 @@ import com.topjohnwu.magisk.di.koinModules
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.unwrap
|
||||
import com.topjohnwu.magisk.utils.RootInit
|
||||
import com.topjohnwu.magisk.utils.SuHandler
|
||||
import com.topjohnwu.magisk.utils.updateConfig
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
@@ -34,6 +35,7 @@ open class App() : Application() {
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootInit::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
FileProvider.callHandler = SuHandler
|
||||
Room.setFactory {
|
||||
when (it) {
|
||||
WorkDatabase::class.java -> WorkDatabase_Impl()
|
||||
|
||||
@@ -26,6 +26,7 @@ object Const {
|
||||
const val MIN_VERSION = "v18.0"
|
||||
const val MIN_VERCODE = 18000
|
||||
const val CONNECT_MODE = 20100
|
||||
const val PROVIDER_CONNECT = 20102
|
||||
}
|
||||
|
||||
object ID {
|
||||
|
||||
@@ -14,8 +14,6 @@ import android.content.res.AssetManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import com.topjohnwu.magisk.extensions.langTagToLocale
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService
|
||||
@@ -23,11 +21,8 @@ import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import com.topjohnwu.magisk.utils.defaultLocale
|
||||
import com.topjohnwu.magisk.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.utils.updateConfig
|
||||
import java.util.*
|
||||
|
||||
fun AssetManager.addAssetPath(path: String) {
|
||||
DynAPK.addAssetPath(this, path)
|
||||
|
||||
@@ -36,17 +36,13 @@ object Info {
|
||||
val str = ShellUtils.fastCmd("magisk -v").split(":".toRegex())[0]
|
||||
val code = ShellUtils.fastCmd("magisk -V").toInt()
|
||||
val hide = Shell.su("magiskhide --status").exec().isSuccess
|
||||
var mode = -1
|
||||
if (code >= Const.Version.CONNECT_MODE)
|
||||
mode = Shell.su("magisk --connect-mode").exec().code
|
||||
Env(code, str, hide, mode)
|
||||
Env(code, str, hide)
|
||||
}.getOrElse { Env() }
|
||||
|
||||
class Env(
|
||||
code: Int = -1,
|
||||
val magiskVersionString: String = "",
|
||||
hide: Boolean = false,
|
||||
var connectionMode: Int = -1
|
||||
hide: Boolean = false
|
||||
) {
|
||||
val magiskHide get() = Config.magiskHide
|
||||
val magiskVersionCode = when (code) {
|
||||
|
||||
@@ -56,14 +56,15 @@ fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Int.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): MagiskPolicy {
|
||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
||||
?: throw PackageManager.NameNotFoundException()
|
||||
val info = pm.getApplicationInfo(pkg, 0)
|
||||
return MagiskPolicy(
|
||||
uid = this,
|
||||
packageName = pkg,
|
||||
policy = policy,
|
||||
applicationInfo = info,
|
||||
appName = info.loadLabel(pm).toString()
|
||||
appName = info.getLabel(pm)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,37 +2,25 @@ package com.topjohnwu.magisk.model.receiver
|
||||
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import com.topjohnwu.magisk.extensions.reboot
|
||||
import com.topjohnwu.magisk.extensions.startActivity
|
||||
import com.topjohnwu.magisk.extensions.startActivityWithRoot
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
import com.topjohnwu.magisk.model.entity.ManagerJson
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.utils.SuLogger
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.utils.SuHandler
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.core.inject
|
||||
import timber.log.Timber
|
||||
|
||||
open class GeneralReceiver : BaseReceiver() {
|
||||
|
||||
private val policyDB: PolicyDao by inject()
|
||||
|
||||
companion object {
|
||||
const val REQUEST = "request"
|
||||
const val LOG = "log"
|
||||
const val NOTIFY = "notify"
|
||||
const val TEST = "test"
|
||||
}
|
||||
|
||||
private fun getPkg(intent: Intent): String {
|
||||
return intent.data?.encodedSchemeSpecificPart.orEmpty()
|
||||
}
|
||||
@@ -40,46 +28,15 @@ open class GeneralReceiver : BaseReceiver() {
|
||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
||||
intent ?: return
|
||||
|
||||
// Debug messages
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.d(intent.action)
|
||||
intent.extras?.let { bundle ->
|
||||
bundle.keySet().forEach {
|
||||
Timber.d("[%s]=[%s]", it, bundle[it])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (intent.action ?: return) {
|
||||
Intent.ACTION_REBOOT -> {
|
||||
when (val action = intent.getStringExtra("action") ?: return) {
|
||||
REQUEST -> {
|
||||
val i = context.intent<SuRequestActivity>()
|
||||
.setAction(action)
|
||||
.putExtra("socket", intent.getStringExtra("socket"))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
if (SDK_INT >= 29) {
|
||||
// Android Q does not allow starting activity from background
|
||||
i.startActivityWithRoot()
|
||||
} else {
|
||||
i.startActivity(context)
|
||||
}
|
||||
}
|
||||
LOG -> SuLogger.handleLogs(context, intent)
|
||||
NOTIFY -> SuLogger.handleNotify(context, intent)
|
||||
TEST -> {
|
||||
val mode = intent.getIntExtra("mode", 1 shl 1)
|
||||
if (mode > Info.env.connectionMode)
|
||||
Info.env.connectionMode = mode
|
||||
Shell.su("magisk --connect-mode $mode").submit()
|
||||
}
|
||||
}
|
||||
SuHandler(context, intent.getStringExtra("action"), intent.extras)
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED ->
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
// This will only work pre-O
|
||||
if (Config.suReAuth)
|
||||
policyDB.delete(getPkg(intent)).blockingGet()
|
||||
}
|
||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
||||
val pkg = getPkg(intent)
|
||||
policyDB.delete(pkg).blockingGet()
|
||||
|
||||
@@ -3,10 +3,13 @@ package com.topjohnwu.magisk.ui
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.intent
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.magisk.wrap
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
open class SplashActivity : Activity() {
|
||||
|
||||
@@ -10,8 +10,7 @@ import com.topjohnwu.magisk.databinding.ActivityRequestBinding
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.events.DieEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
import com.topjohnwu.magisk.utils.SuLogger
|
||||
import com.topjohnwu.magisk.utils.SuHandler
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestBinding>() {
|
||||
@@ -29,19 +28,13 @@ open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestB
|
||||
lockOrientation()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val intent = intent
|
||||
|
||||
when (intent?.action) {
|
||||
GeneralReceiver.REQUEST -> {
|
||||
if (!viewModel.handleRequest(intent))
|
||||
finish()
|
||||
return
|
||||
}
|
||||
GeneralReceiver.LOG -> SuLogger.handleLogs(this, intent)
|
||||
GeneralReceiver.NOTIFY -> SuLogger.handleNotify(this, intent)
|
||||
if (intent?.action == SuHandler.REQUEST) {
|
||||
if (!viewModel.handleRequest(intent))
|
||||
finish()
|
||||
} else {
|
||||
SuHandler(this, intent.action, intent.extras)
|
||||
finish()
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onEventDispatched(event: ViewEvent) {
|
||||
|
||||
@@ -77,8 +77,9 @@ object PatchAPK {
|
||||
}
|
||||
|
||||
private fun patchAndHide(context: Context, label: String): Boolean {
|
||||
// If not running as stub, and we are compatible with stub, use stub
|
||||
val src = if (!isRunningAsStub && SDK_INT >= 28 && Info.env.connectionMode == 3) {
|
||||
val src = if (!isRunningAsStub && SDK_INT >= 28 &&
|
||||
Info.env.magiskVersionCode >= Const.Version.PROVIDER_CONNECT) {
|
||||
// If not running as stub, and we are compatible with stub, use stub
|
||||
val stub = File(context.cacheDir, "stub.apk")
|
||||
val svc = get<GithubRawServices>()
|
||||
runCatching {
|
||||
|
||||
137
app/src/main/java/com/topjohnwu/magisk/utils/SuHandler.kt
Normal file
137
app/src/main/java/com/topjohnwu/magisk/utils/SuHandler.kt
Normal file
@@ -0,0 +1,137 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.startActivity
|
||||
import com.topjohnwu.magisk.extensions.startActivityWithRoot
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
object SuHandler : ProviderCallHandler {
|
||||
|
||||
const val REQUEST = "request"
|
||||
const val LOG = "log"
|
||||
const val NOTIFY = "notify"
|
||||
const val TEST = "test"
|
||||
|
||||
override fun call(context: Context, method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
invoke(context.wrap(), method, extras)
|
||||
return null
|
||||
}
|
||||
|
||||
operator fun invoke(context: Context, action: String?, data: Bundle?) {
|
||||
data ?: return
|
||||
|
||||
// Debug messages
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.d(action)
|
||||
data.let { bundle ->
|
||||
bundle.keySet().forEach {
|
||||
Timber.d("[%s]=[%s]", it, bundle[it])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (action) {
|
||||
REQUEST -> {
|
||||
val intent = context.intent<SuRequestActivity>()
|
||||
.setAction(action)
|
||||
.putExtras(data)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
// Android Q does not allow starting activity from background
|
||||
intent.startActivityWithRoot()
|
||||
} else {
|
||||
intent.startActivity(context)
|
||||
}
|
||||
}
|
||||
LOG -> handleLogs(context, data)
|
||||
NOTIFY -> handleNotify(context, data)
|
||||
TEST -> {
|
||||
val mode = data.getInt("mode", 2)
|
||||
Shell.su(
|
||||
"magisk --connect-mode $mode",
|
||||
"magisk --use-broadcast"
|
||||
).submit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Any?.toInt(): Int? {
|
||||
return when (this) {
|
||||
is Int -> this
|
||||
is Long -> this.toInt()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLogs(context: Context, data: Bundle) {
|
||||
val fromUid = data["from.uid"].toInt() ?: return
|
||||
if (fromUid == Process.myUid())
|
||||
return
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val notify = data.getBoolean("notify", true)
|
||||
val allow = data["policy"].toInt() ?: return
|
||||
|
||||
val policy = runCatching { fromUid.toPolicy(pm, allow) }.getOrElse { return }
|
||||
|
||||
if (notify)
|
||||
notify(context, policy)
|
||||
|
||||
val toUid = data["to.uid"].toInt() ?: return
|
||||
val pid = data["pid"].toInt() ?: return
|
||||
|
||||
val command = data.getString("command") ?: return
|
||||
val log = policy.toLog(
|
||||
toUid = toUid,
|
||||
fromPid = pid,
|
||||
command = command,
|
||||
date = Date()
|
||||
)
|
||||
|
||||
val logRepo = get<LogRepository>()
|
||||
logRepo.put(log).subscribeK(onError = { Timber.e(it) })
|
||||
}
|
||||
|
||||
private fun handleNotify(context: Context, data: Bundle) {
|
||||
val fromUid = data["from.uid"].toInt() ?: return
|
||||
if (fromUid == Process.myUid())
|
||||
return
|
||||
|
||||
val pm = context.packageManager
|
||||
val allow = data["policy"].toInt() ?: return
|
||||
|
||||
runCatching {
|
||||
val policy = fromUid.toPolicy(pm, allow)
|
||||
if (policy.policy >= 0)
|
||||
notify(context, policy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(context: Context, policy: MagiskPolicy) {
|
||||
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||
val resId = if (policy.policy == MagiskPolicy.ALLOW)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
R.string.su_deny_toast
|
||||
|
||||
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Process
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import java.util.*
|
||||
|
||||
object SuLogger {
|
||||
|
||||
fun handleLogs(context: Context, intent: Intent) {
|
||||
|
||||
val fromUid = intent.getIntExtra("from.uid", -1)
|
||||
if (fromUid < 0) return
|
||||
if (fromUid == Process.myUid()) return
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val notify: Boolean
|
||||
val data = intent.extras
|
||||
val policy: MagiskPolicy = if (data!!.containsKey("notify")) {
|
||||
notify = data.getBoolean("notify")
|
||||
runCatching {
|
||||
fromUid.toPolicy(pm)
|
||||
}.getOrElse { return }
|
||||
} else {
|
||||
// Doesn't report whether notify or not, check database ourselves
|
||||
val policyDB = get<PolicyDao>()
|
||||
val policy = policyDB.fetch(fromUid).blockingGet() ?: return
|
||||
notify = policy.notification
|
||||
policy
|
||||
}.copy(policy = data.getInt("policy", -1))
|
||||
|
||||
if (policy.policy < 0)
|
||||
return
|
||||
|
||||
if (notify)
|
||||
handleNotify(context, policy)
|
||||
|
||||
val toUid = intent.getIntExtra("to.uid", -1)
|
||||
if (toUid < 0) return
|
||||
|
||||
val pid = intent.getIntExtra("pid", -1)
|
||||
if (pid < 0) return
|
||||
|
||||
val command = intent.getStringExtra("command") ?: return
|
||||
val log = policy.toLog(
|
||||
toUid = toUid,
|
||||
fromPid = pid,
|
||||
command = command,
|
||||
date = Date()
|
||||
)
|
||||
|
||||
val logRepo = get<LogRepository>()
|
||||
logRepo.put(log).blockingGet()?.printStackTrace()
|
||||
}
|
||||
|
||||
private fun handleNotify(context: Context, policy: MagiskPolicy) {
|
||||
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||
Utils.toast(
|
||||
context.getString(
|
||||
if (policy.policy == MagiskPolicy.ALLOW)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
R.string.su_deny_toast, policy.appName
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleNotify(context: Context, intent: Intent) {
|
||||
val fromUid = intent.getIntExtra("from.uid", -1)
|
||||
if (fromUid < 0) return
|
||||
if (fromUid == Process.myUid()) return
|
||||
runCatching {
|
||||
val pm = context.packageManager
|
||||
val policy = fromUid.toPolicy(pm)
|
||||
.copy(policy = intent.getIntExtra("policy", -1))
|
||||
if (policy.policy >= 0)
|
||||
handleNotify(context, policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user