mirror of
https://github.com/topjohnwu/Magisk
synced 2025-11-03 15:52:30 +01:00
Compare commits
71 Commits
canary-280
...
canary-281
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b62835cbeb | ||
|
|
6ea740b5ab | ||
|
|
7ab98dd5ac | ||
|
|
fc8b3400fc | ||
|
|
54428ba415 | ||
|
|
95d1e69d8e | ||
|
|
a0f13ab49f | ||
|
|
c3e8405020 | ||
|
|
a93593ea66 | ||
|
|
23eff70883 | ||
|
|
110dd4a8b9 | ||
|
|
d9c2bffc9f | ||
|
|
049db49dc8 | ||
|
|
7c1d2ec61e | ||
|
|
a1b2830c06 | ||
|
|
82d1d19267 | ||
|
|
4d4195c02d | ||
|
|
5637a258fc | ||
|
|
ee6810f417 | ||
|
|
7098248c64 | ||
|
|
0d31d356ef | ||
|
|
b782e7dcb7 | ||
|
|
a4671b4698 | ||
|
|
7edd8be169 | ||
|
|
24650eefe4 | ||
|
|
8e1a44e7eb | ||
|
|
2722875190 | ||
|
|
3ca6d06f69 | ||
|
|
10e47248de | ||
|
|
e73ff679ac | ||
|
|
53e401fa2d | ||
|
|
d2768357da | ||
|
|
a6c2ba7c1e | ||
|
|
aae5b466fb | ||
|
|
2b7be8b949 | ||
|
|
b6511a510d | ||
|
|
704541aef2 | ||
|
|
005560a4c5 | ||
|
|
231a5d1853 | ||
|
|
9e2b59060d | ||
|
|
08ea937f7c | ||
|
|
2baedf74d1 | ||
|
|
32faa4ced6 | ||
|
|
ccdb0b5d13 | ||
|
|
8506b672ad | ||
|
|
ce2e33bb20 | ||
|
|
6707b72260 | ||
|
|
5885b8c20d | ||
|
|
820710c086 | ||
|
|
51cf196bf7 | ||
|
|
dadba44cf9 | ||
|
|
2ce4a5543b | ||
|
|
9112a3a4f5 | ||
|
|
24615afda1 | ||
|
|
c5778f398b | ||
|
|
4eb4097b9b | ||
|
|
c512496847 | ||
|
|
506961a10d | ||
|
|
3414415907 | ||
|
|
dc2ae7cfd1 | ||
|
|
2e86d21c29 | ||
|
|
2654382c43 | ||
|
|
9e26b73813 | ||
|
|
10cd13bf80 | ||
|
|
f10ee5f887 | ||
|
|
47cc532d96 | ||
|
|
218327f92b | ||
|
|
4eae66a1a7 | ||
|
|
b09ceeb43c | ||
|
|
4fb539c110 | ||
|
|
849b284da5 |
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
@@ -1,15 +1,6 @@
|
|||||||
name: Magisk Build
|
name: Magisk Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
paths:
|
|
||||||
- "app/**"
|
|
||||||
- "native/**"
|
|
||||||
- "buildSrc/**"
|
|
||||||
- "build.py"
|
|
||||||
- "gradle.properties"
|
|
||||||
- ".github/workflows/build.yml"
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -17,7 +8,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build Magisk artifacts
|
name: Build Magisk artifacts
|
||||||
runs-on: macos-14
|
runs-on: macos-15
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
@@ -60,7 +51,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest, ubuntu-latest]
|
os: [windows-2025, ubuntu-24.04]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -78,7 +69,7 @@ jobs:
|
|||||||
|
|
||||||
avd-test:
|
avd-test:
|
||||||
name: Test API ${{ matrix.version }} (x86_64)
|
name: Test API ${{ matrix.version }} (x86_64)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
needs: build
|
needs: build
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -88,6 +79,8 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- version: "Baklava"
|
- version: "Baklava"
|
||||||
type: "google_apis"
|
type: "google_apis"
|
||||||
|
- version: "Baklava"
|
||||||
|
type: "google_apis_ps16k"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
@@ -122,7 +115,7 @@ jobs:
|
|||||||
|
|
||||||
avd-test-32:
|
avd-test-32:
|
||||||
name: Test API ${{ matrix.version }} (x86)
|
name: Test API ${{ matrix.version }} (x86)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
needs: build
|
needs: build
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -161,7 +154,7 @@ jobs:
|
|||||||
kernel.log
|
kernel.log
|
||||||
logcat.log
|
logcat.log
|
||||||
|
|
||||||
cf_test:
|
cf-test:
|
||||||
name: Test ${{ matrix.device }}
|
name: Test ${{ matrix.device }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs: build
|
needs: build
|
||||||
@@ -173,8 +166,6 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- branch: "aosp-main"
|
- branch: "aosp-main"
|
||||||
device: "aosp_cf_x86_64_phone"
|
device: "aosp_cf_x86_64_phone"
|
||||||
- branch: "aosp-main-throttled"
|
|
||||||
device: "aosp_cf_x86_64_phone_pgagnostic"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ Some highlight features:
|
|||||||
|
|
||||||
Click the icon below to download Magisk apk.
|
Click the icon below to download Magisk apk.
|
||||||
|
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v27.0)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v28.1)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v28.0)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v28.1)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/canary-28003)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/canary-28102)
|
||||||
|
|
||||||
## Useful Links
|
## Useful Links
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.topjohnwu.magisk.BR
|
|||||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||||
import com.topjohnwu.magisk.core.AppContext
|
import com.topjohnwu.magisk.core.AppContext
|
||||||
import com.topjohnwu.magisk.core.BuildConfig
|
import com.topjohnwu.magisk.core.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.R
|
import com.topjohnwu.magisk.core.R
|
||||||
@@ -92,7 +93,7 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
|
|||||||
DownloadPath -> withExternalRW(doAction)
|
DownloadPath -> withExternalRW(doAction)
|
||||||
UpdateChecker -> withPostNotificationPermission(doAction)
|
UpdateChecker -> withPostNotificationPermission(doAction)
|
||||||
Authentication -> AuthEvent(doAction).publish()
|
Authentication -> AuthEvent(doAction).publish()
|
||||||
Hide, Restore -> withInstallPermission(doAction)
|
AutomaticResponse -> if (Config.suAuth) AuthEvent(doAction).publish() else doAction()
|
||||||
else -> doAction()
|
else -> doAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,5 +60,10 @@ dependencies {
|
|||||||
implementation(libs.activity)
|
implementation(libs.activity)
|
||||||
implementation(libs.collection.ktx)
|
implementation(libs.collection.ktx)
|
||||||
implementation(libs.profileinstaller)
|
implementation(libs.profileinstaller)
|
||||||
implementation(libs.lifecycle.process)
|
|
||||||
|
// We also implement all our tests in this module.
|
||||||
|
// However, we don't want to bundle test dependencies.
|
||||||
|
// That's why we make it compileOnly.
|
||||||
|
compileOnly(libs.test.junit)
|
||||||
|
compileOnly(libs.test.uiautomator)
|
||||||
}
|
}
|
||||||
|
|||||||
10
app/core/proguard-rules.pro
vendored
10
app/core/proguard-rules.pro
vendored
@@ -37,12 +37,4 @@
|
|||||||
-flattenpackagehierarchy
|
-flattenpackagehierarchy
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
|
|
||||||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
-dontwarn org.junit.**
|
||||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
|
||||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
|
||||||
-dontwarn org.commonmark.ext.gfm.strikethrough.Strikethrough
|
|
||||||
-dontwarn org.conscrypt.Conscrypt*
|
|
||||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
|
||||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
|
||||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
|
||||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import com.topjohnwu.magisk.StubApk
|
|||||||
import com.topjohnwu.magisk.core.base.UntrackedActivity
|
import com.topjohnwu.magisk.core.base.UntrackedActivity
|
||||||
import com.topjohnwu.magisk.core.utils.LocaleSetting
|
import com.topjohnwu.magisk.core.utils.LocaleSetting
|
||||||
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
||||||
import com.topjohnwu.magisk.core.utils.ProcessLifecycle
|
|
||||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.core.utils.ShellInit
|
import com.topjohnwu.magisk.core.utils.ShellInit
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
@@ -40,6 +39,7 @@ object AppContext : ContextWrapper(null),
|
|||||||
|
|
||||||
private var ref = WeakReference<Activity>(null)
|
private var ref = WeakReference<Activity>(null)
|
||||||
private lateinit var application: Application
|
private lateinit var application: Application
|
||||||
|
private lateinit var networkObserver: NetworkObserver
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Always log full stack trace with Timber
|
// Always log full stack trace with Timber
|
||||||
@@ -56,6 +56,10 @@ object AppContext : ContextWrapper(null),
|
|||||||
LocaleSetting.instance.updateResource(resources)
|
LocaleSetting.instance.updateResource(resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onActivityStarted(activity: Activity) {
|
||||||
|
networkObserver.postCurrentState()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
override fun onActivityResumed(activity: Activity) {
|
||||||
if (activity is UntrackedActivity) return
|
if (activity is UntrackedActivity) return
|
||||||
ref = WeakReference(activity)
|
ref = WeakReference(activity)
|
||||||
@@ -102,8 +106,7 @@ object AppContext : ContextWrapper(null),
|
|||||||
val lm = getSystemService(LocaleManager::class.java)
|
val lm = getSystemService(LocaleManager::class.java)
|
||||||
lm.overrideLocaleConfig = LocaleSetting.localeConfig
|
lm.overrideLocaleConfig = LocaleSetting.localeConfig
|
||||||
}
|
}
|
||||||
ProcessLifecycle.init(this)
|
networkObserver = NetworkObserver.init(this)
|
||||||
NetworkObserver.init(this)
|
|
||||||
if (!BuildConfig.DEBUG && !isRunningAsStub) {
|
if (!BuildConfig.DEBUG && !isRunningAsStub) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
ProfileInstaller.writeProfile(this@AppContext)
|
ProfileInstaller.writeProfile(this@AppContext)
|
||||||
@@ -120,7 +123,6 @@ object AppContext : ContextWrapper(null),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||||
override fun onActivityStarted(activity: Activity) {}
|
|
||||||
override fun onActivityStopped(activity: Activity) {}
|
override fun onActivityStopped(activity: Activity) {}
|
||||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||||
override fun onActivityDestroyed(activity: Activity) {}
|
override fun onActivityDestroyed(activity: Activity) {}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ object Config : PreferenceConfig, DBConfig {
|
|||||||
const val SU_AUTO_ALLOW = 2
|
const val SU_AUTO_ALLOW = 2
|
||||||
|
|
||||||
// su timeout
|
// su timeout
|
||||||
val TIMEOUT_LIST = intArrayOf(0, -1, 10, 20, 30, 60)
|
val TIMEOUT_LIST = longArrayOf(0, -1, 10, 20, 30, 60)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val defaultChannel =
|
private val defaultChannel =
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.core.content.getSystemService
|
|||||||
import com.topjohnwu.magisk.core.base.BaseJobService
|
import com.topjohnwu.magisk.core.base.BaseJobService
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||||
|
import com.topjohnwu.magisk.core.download.DownloadSession
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -25,7 +26,7 @@ class JobService : BaseJobService() {
|
|||||||
@TargetApi(value = 34)
|
@TargetApi(value = 34)
|
||||||
inner class Session(
|
inner class Session(
|
||||||
private var params: JobParameters
|
private var params: JobParameters
|
||||||
) : DownloadEngine.Session {
|
) : DownloadSession {
|
||||||
|
|
||||||
override val context get() = this@JobService
|
override val context get() = this@JobService
|
||||||
val engine = DownloadEngine(this)
|
val engine = DownloadEngine(this)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.topjohnwu.magisk.core
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.topjohnwu.magisk.core.base.BaseProvider
|
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||||
import com.topjohnwu.magisk.core.su.TestHandler
|
|
||||||
|
|
||||||
class Provider : BaseProvider() {
|
class Provider : BaseProvider() {
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ class Provider : BaseProvider() {
|
|||||||
SuCallbackHandler.run(context!!, method, extras)
|
SuCallbackHandler.run(context!!, method, extras)
|
||||||
Bundle.EMPTY
|
Bundle.EMPTY
|
||||||
}
|
}
|
||||||
else -> TestHandler.run(method)
|
else -> Bundle.EMPTY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import androidx.core.app.ServiceCompat
|
|||||||
import androidx.core.content.IntentCompat
|
import androidx.core.content.IntentCompat
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
import com.topjohnwu.magisk.core.base.BaseService
|
||||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||||
|
import com.topjohnwu.magisk.core.download.DownloadSession
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
|
|
||||||
class Service : BaseService(), DownloadEngine.Session {
|
class Service : BaseService(), DownloadSession {
|
||||||
|
|
||||||
private var mEngine: DownloadEngine? = null
|
private var mEngine: DownloadEngine? = null
|
||||||
override val context get() = this
|
override val context get() = this
|
||||||
|
|||||||
@@ -7,9 +7,13 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
open class MagiskDB {
|
open class MagiskDB {
|
||||||
|
|
||||||
suspend fun <R> exec(
|
class Literal(
|
||||||
|
val str: String
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend inline fun <R> exec(
|
||||||
query: String,
|
query: String,
|
||||||
mapper: suspend (Map<String, String>) -> R
|
crossinline mapper: (Map<String, String>) -> R
|
||||||
): List<R> {
|
): List<R> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val out = Shell.cmd("magisk --sqlite '$query'").await().out
|
val out = Shell.cmd("magisk --sqlite '$query'").await().out
|
||||||
@@ -18,13 +22,15 @@ open class MagiskDB {
|
|||||||
.map { it.split("=", limit = 2) }
|
.map { it.split("=", limit = 2) }
|
||||||
.filter { it.size == 2 }
|
.filter { it.size == 2 }
|
||||||
.associate { it[0] to it[1] }
|
.associate { it[0] to it[1] }
|
||||||
.let { mapper(it) }
|
.let(mapper)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun exec(query: String) {
|
suspend fun exec(query: String) {
|
||||||
exec(query) {}
|
withContext(Dispatchers.IO) {
|
||||||
|
Shell.cmd("magisk --sqlite '$query'").await()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Map<String, Any>.toQuery(): String {
|
fun Map<String, Any>.toQuery(): String {
|
||||||
@@ -33,6 +39,7 @@ open class MagiskDB {
|
|||||||
when (it) {
|
when (it) {
|
||||||
is Boolean -> if (it) "1" else "0"
|
is Boolean -> if (it) "1" else "0"
|
||||||
is Number -> it.toString()
|
is Number -> it.toString()
|
||||||
|
is Literal -> it.str
|
||||||
else -> "\"$it\""
|
else -> "\"$it\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,24 @@ package com.topjohnwu.magisk.core.data.magiskdb
|
|||||||
import com.topjohnwu.magisk.core.AppContext
|
import com.topjohnwu.magisk.core.AppContext
|
||||||
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 java.util.concurrent.TimeUnit
|
|
||||||
|
private const val SELECT_QUERY = "SELECT (until - strftime(\"%s\", \"now\")) AS remain, *"
|
||||||
|
|
||||||
class PolicyDao : MagiskDB() {
|
class PolicyDao : MagiskDB() {
|
||||||
|
|
||||||
suspend fun deleteOutdated() {
|
suspend fun deleteOutdated() {
|
||||||
val nowSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
|
|
||||||
val query = "DELETE FROM ${Table.POLICY} WHERE " +
|
val query = "DELETE FROM ${Table.POLICY} WHERE " +
|
||||||
"(until > 0 AND until < $nowSeconds) OR until < 0"
|
"(until > 0 AND until < strftime(\"%s\", \"now\")) OR until < 0"
|
||||||
exec(query)
|
exec(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun delete(uid: Int) {
|
suspend fun delete(uid: Int) {
|
||||||
val query = "DELETE FROM ${Table.POLICY} WHERE uid == $uid"
|
val query = "DELETE FROM ${Table.POLICY} WHERE uid=$uid"
|
||||||
exec(query)
|
exec(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
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_QUERY FROM ${Table.POLICY} WHERE uid=$uid LIMIT 1"
|
||||||
return exec(query, ::toPolicy).firstOrNull()
|
return exec(query, ::toPolicy).firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class PolicyDao : MagiskDB() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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_QUERY FROM ${Table.POLICY} WHERE uid/100000=${Const.USER_ID}"
|
||||||
return exec(query, ::toPolicy).filterNotNull()
|
return exec(query, ::toPolicy).filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,8 +43,15 @@ class PolicyDao : MagiskDB() {
|
|||||||
val uid = map["uid"]?.toInt() ?: return null
|
val uid = map["uid"]?.toInt() ?: return null
|
||||||
val policy = SuPolicy(uid)
|
val policy = SuPolicy(uid)
|
||||||
|
|
||||||
|
map["until"]?.toLong()?.let { until ->
|
||||||
|
if (until <= 0) {
|
||||||
|
policy.remain = until
|
||||||
|
} else {
|
||||||
|
map["remain"]?.toLong()?.let { policy.remain = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
map["policy"]?.toInt()?.let { policy.policy = it }
|
map["policy"]?.toInt()?.let { policy.policy = it }
|
||||||
map["until"]?.toLong()?.let { policy.until = it }
|
|
||||||
map["logging"]?.toInt()?.let { policy.logging = it != 0 }
|
map["logging"]?.toInt()?.let { policy.logging = it != 0 }
|
||||||
map["notification"]?.toInt()?.let { policy.notification = it != 0 }
|
map["notification"]?.toInt()?.let { policy.notification = it != 0 }
|
||||||
return policy
|
return policy
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.topjohnwu.magisk.core.data.magiskdb
|
|||||||
class SettingsDao : MagiskDB() {
|
class SettingsDao : MagiskDB() {
|
||||||
|
|
||||||
suspend fun delete(key: String) {
|
suspend fun delete(key: String) {
|
||||||
val query = "DELETE FROM ${Table.SETTINGS} WHERE key == \"$key\""
|
val query = "DELETE FROM ${Table.SETTINGS} WHERE key=\"$key\""
|
||||||
exec(query)
|
exec(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ class SettingsDao : MagiskDB() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetch(key: String, default: Int = -1): Int {
|
suspend fun fetch(key: String, default: Int = -1): Int {
|
||||||
val query = "SELECT value FROM ${Table.SETTINGS} WHERE key == \"$key\" LIMIT 1"
|
val query = "SELECT value FROM ${Table.SETTINGS} WHERE key=\"$key\" LIMIT 1"
|
||||||
return exec(query) { it["value"]?.toInt() }.firstOrNull() ?: default
|
return exec(query) { it["value"]?.toInt() }.firstOrNull() ?: default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.topjohnwu.magisk.core.data.magiskdb
|
|||||||
class StringDao : MagiskDB() {
|
class StringDao : MagiskDB() {
|
||||||
|
|
||||||
suspend fun delete(key: String) {
|
suspend fun delete(key: String) {
|
||||||
val query = "DELETE FROM ${Table.STRINGS} WHERE key == \"$key\""
|
val query = "DELETE FROM ${Table.STRINGS} WHERE key=\"$key\""
|
||||||
exec(query)
|
exec(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ class StringDao : MagiskDB() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetch(key: String, default: String = ""): String {
|
suspend fun fetch(key: String, default: String = ""): String {
|
||||||
val query = "SELECT value FROM ${Table.STRINGS} WHERE key == \"$key\" LIMIT 1"
|
val query = "SELECT value FROM ${Table.STRINGS} WHERE key=\"$key\" LIMIT 1"
|
||||||
return exec(query) { it["value"] }.firstOrNull() ?: default
|
return exec(query) { it["value"] }.firstOrNull() ?: default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
|||||||
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.R
|
||||||
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
|
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||||
|
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||||
|
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||||
|
import com.topjohnwu.magisk.core.ktx.withInOut
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
|
import com.topjohnwu.magisk.core.tasks.AppMigration
|
||||||
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
class DownloadProcessor(notifier: DownloadNotifier) : DownloadNotifier by notifier {
|
||||||
|
|
||||||
|
suspend fun handle(stream: InputStream, subject: Subject) {
|
||||||
|
when (subject) {
|
||||||
|
is Subject.App -> handleApp(stream, subject)
|
||||||
|
is Subject.Module -> handleModule(stream, subject.file)
|
||||||
|
else -> stream.copyAndClose(subject.file.outputStream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||||
|
val external = subject.file.outputStream()
|
||||||
|
|
||||||
|
if (isRunningAsStub) {
|
||||||
|
val updateApk = StubApk.update(context)
|
||||||
|
try {
|
||||||
|
// Download full APK to stub update path
|
||||||
|
stream.copyAndClose(TeeOutputStream(external, updateApk.outputStream()))
|
||||||
|
|
||||||
|
// Also upgrade stub
|
||||||
|
notifyUpdate(subject.notifyId) {
|
||||||
|
it.setProgress(0, 0, true)
|
||||||
|
.setContentTitle(context.getString(R.string.hide_app_title))
|
||||||
|
.setContentText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract stub
|
||||||
|
val apk = context.cachedFile("stub.apk")
|
||||||
|
ZipFile.Builder().setFile(updateApk).get().use { zf ->
|
||||||
|
apk.delete()
|
||||||
|
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch and install
|
||||||
|
subject.intent = AppMigration.upgradeStub(context, apk)
|
||||||
|
?: throw IOException("HideAPK patch error")
|
||||||
|
apk.delete()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If any error occurred, do not let stub load the new APK
|
||||||
|
updateApk.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val session = APKInstall.startSession(context)
|
||||||
|
stream.copyAndClose(TeeOutputStream(external, session.openStream(context)))
|
||||||
|
subject.intent = session.waitIntent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun handleModule(src: InputStream, file: Uri) {
|
||||||
|
val tmp = context.cachedFile("module.zip")
|
||||||
|
try {
|
||||||
|
// First download the entire zip into cache so we can process it
|
||||||
|
src.writeTo(tmp)
|
||||||
|
|
||||||
|
val input = ZipFile.Builder().setFile(tmp).get()
|
||||||
|
val output = ZipArchiveOutputStream(file.outputStream())
|
||||||
|
withInOut(input, output) { zin, zout ->
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/"))
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/"))
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/"))
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/"))
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/update-binary"))
|
||||||
|
context.assets.open("module_installer.sh").use { it.copyAll(zout) }
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
|
||||||
|
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/updater-script"))
|
||||||
|
zout.write("#MAGISK\n".toByteArray())
|
||||||
|
zout.closeArchiveEntry()
|
||||||
|
|
||||||
|
// Then simply copy all entries to output
|
||||||
|
zin.copyRawEntries(zout) { entry -> !entry.name.startsWith("META-INF") }
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
tmp.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TeeOutputStream(
|
||||||
|
private val o1: OutputStream,
|
||||||
|
private val o2: OutputStream
|
||||||
|
) : OutputStream() {
|
||||||
|
override fun write(b: Int) {
|
||||||
|
o1.write(b)
|
||||||
|
o2.write(b)
|
||||||
|
}
|
||||||
|
override fun write(b: ByteArray?, off: Int, len: Int) {
|
||||||
|
o1.write(b, off, len)
|
||||||
|
o2.write(b, off, len)
|
||||||
|
}
|
||||||
|
override fun close() {
|
||||||
|
o1.close()
|
||||||
|
o2.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
interface DownloadSession {
|
||||||
|
val context: Context
|
||||||
|
fun attachNotification(id: Int, builder: Notification.Builder)
|
||||||
|
fun onDownloadComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadNotifier {
|
||||||
|
val context: Context
|
||||||
|
fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {})
|
||||||
|
}
|
||||||
@@ -2,7 +2,11 @@ package com.topjohnwu.magisk.core.ktx
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.*
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
@@ -23,7 +27,6 @@ import com.topjohnwu.magisk.core.utils.RootUtils
|
|||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.String
|
|
||||||
|
|
||||||
fun Context.getBitmap(id: Int): Bitmap {
|
fun Context.getBitmap(id: Int): Bitmap {
|
||||||
var drawable = getDrawable(id)!!
|
var drawable = getDrawable(id)!!
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.flatMapMerge
|
|||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.Closeable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -17,24 +18,14 @@ import java.text.DateFormat
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
|
|
||||||
inline fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
inline fun <In : Closeable, Out : Closeable> withInOut(
|
||||||
var entry: ZipEntry? = nextEntry
|
input: In,
|
||||||
while (entry != null) {
|
output: Out,
|
||||||
callback(entry)
|
|
||||||
entry = nextEntry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
|
||||||
inStream: In,
|
|
||||||
outStream: Out,
|
|
||||||
withBoth: (In, Out) -> Unit
|
withBoth: (In, Out) -> Unit
|
||||||
) {
|
) {
|
||||||
inStream.use { reader ->
|
input.use { reader ->
|
||||||
outStream.use { writer ->
|
output.use { writer ->
|
||||||
withBoth(reader, writer)
|
withBoth(reader, writer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +55,7 @@ suspend inline fun InputStream.copyAndClose(
|
|||||||
out: OutputStream,
|
out: OutputStream,
|
||||||
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
) = withStreams(this, out) { i, o -> i.copyAll(o, bufferSize, dispatcher) }
|
) = withInOut(this, out) { i, o -> i.copyAll(o, bufferSize, dispatcher) }
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
suspend inline fun InputStream.writeTo(
|
suspend inline fun InputStream.writeTo(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
data class LocalModule(
|
data class LocalModule(
|
||||||
private val path: String,
|
private val path: String,
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
package com.topjohnwu.magisk.core.model.su
|
package com.topjohnwu.magisk.core.model.su
|
||||||
|
|
||||||
class SuPolicy(val uid: Int) {
|
import com.topjohnwu.magisk.core.data.magiskdb.MagiskDB
|
||||||
|
|
||||||
|
class SuPolicy(
|
||||||
|
val uid: Int,
|
||||||
|
var policy: Int = INTERACTIVE,
|
||||||
|
var remain: 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
|
||||||
}
|
}
|
||||||
|
|
||||||
var policy: Int = INTERACTIVE
|
fun toMap(): MutableMap<String, Any> {
|
||||||
var until: Long = -1L
|
val until = if (remain <= 0) {
|
||||||
var logging: Boolean = true
|
remain
|
||||||
var notification: Boolean = true
|
} else {
|
||||||
|
MagiskDB.Literal("(strftime(\"%s\", \"now\") + $remain)")
|
||||||
fun toMap(): MutableMap<String, Any> = mutableMapOf(
|
}
|
||||||
"uid" to uid,
|
return mutableMapOf(
|
||||||
"policy" to policy,
|
"uid" to uid,
|
||||||
"until" to until,
|
"policy" to policy,
|
||||||
"logging" to logging,
|
"until" to until,
|
||||||
"notification" to notification
|
"logging" to logging,
|
||||||
)
|
"notification" to notification
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class SuRequestHandler(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
output = File(fifo)
|
output = File(fifo)
|
||||||
policy = SuPolicy(uid)
|
policy = policyDB.fetch(uid) ?: SuPolicy(uid)
|
||||||
try {
|
try {
|
||||||
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
||||||
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||||
@@ -81,15 +81,13 @@ class SuRequestHandler(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun respond(action: Int, time: Int) {
|
suspend fun respond(action: Int, time: Long) {
|
||||||
val until = if (time > 0)
|
|
||||||
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) +
|
|
||||||
TimeUnit.MINUTES.toSeconds(time.toLong())
|
|
||||||
else
|
|
||||||
time.toLong()
|
|
||||||
|
|
||||||
policy.policy = action
|
policy.policy = action
|
||||||
policy.until = until
|
if (time >= 0) {
|
||||||
|
policy.remain = TimeUnit.MINUTES.toSeconds(time)
|
||||||
|
} else {
|
||||||
|
policy.remain = time
|
||||||
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -100,7 +98,7 @@ class SuRequestHandler(
|
|||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
if (until >= 0) {
|
if (time >= 0) {
|
||||||
policyDB.update(policy)
|
policyDB.update(policy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.su
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.topjohnwu.magisk.core.Config
|
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
|
||||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
|
||||||
import com.topjohnwu.superuser.CallbackList
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Runnable
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
object TestHandler {
|
|
||||||
|
|
||||||
object LogList : CallbackList<String>(Runnable::run) {
|
|
||||||
override fun onAddElement(e: String) {
|
|
||||||
Timber.i(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun run(method: String): Bundle {
|
|
||||||
var reason: String? = null
|
|
||||||
|
|
||||||
fun prerequisite(): Boolean {
|
|
||||||
// Make sure the Magisk app can get root
|
|
||||||
val shell = Shell.getShell()
|
|
||||||
if (!shell.isRoot) {
|
|
||||||
reason = "shell not root"
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the root service is running
|
|
||||||
RootUtils.Connection.await()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setup(): Boolean {
|
|
||||||
return runBlocking {
|
|
||||||
MagiskInstaller.Emulator(LogList, LogList).exec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun test(): Boolean {
|
|
||||||
// Make sure Zygisk works correctly
|
|
||||||
if (!Info.isZygiskEnabled) {
|
|
||||||
reason = "zygisk not enabled"
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear existing grant for ADB shell
|
|
||||||
runBlocking {
|
|
||||||
ServiceLocator.policyDB.delete(2000)
|
|
||||||
Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW
|
|
||||||
Config.prefs.edit().commit()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = prerequisite() && runCatching {
|
|
||||||
when (method) {
|
|
||||||
"setup" -> setup()
|
|
||||||
"test" -> test()
|
|
||||||
else -> {
|
|
||||||
reason = "unknown method"
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.getOrElse {
|
|
||||||
reason = it.stackTraceToString()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
return Bundle().apply {
|
|
||||||
putBoolean("result", result)
|
|
||||||
if (reason != null) putString("reason", reason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import android.app.Activity
|
|||||||
import android.app.ActivityOptions
|
import android.app.ActivityOptions
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
@@ -13,7 +14,6 @@ import com.topjohnwu.magisk.core.Config
|
|||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.R
|
import com.topjohnwu.magisk.core.R
|
||||||
import com.topjohnwu.magisk.core.ktx.await
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
|
||||||
import com.topjohnwu.magisk.core.ktx.toast
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.signing.JarMap
|
import com.topjohnwu.magisk.core.signing.JarMap
|
||||||
@@ -23,11 +23,9 @@ import com.topjohnwu.magisk.core.utils.Keygen
|
|||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Runnable
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
@@ -38,6 +36,7 @@ object AppMigration {
|
|||||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
||||||
private const val ALPHADOTS = "$ALPHA....."
|
private const val ALPHADOTS = "$ALPHA....."
|
||||||
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||||
|
private const val TEST_PKG_NAME = "$APP_PACKAGE_NAME.test"
|
||||||
|
|
||||||
// Some arbitrary limit
|
// Some arbitrary limit
|
||||||
const val MAX_LABEL_LENGTH = 32
|
const val MAX_LABEL_LENGTH = 32
|
||||||
@@ -133,21 +132,15 @@ object AppMigration {
|
|||||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||||
val xml = AXML(jar.getRawData(je))
|
val xml = AXML(jar.getRawData(je))
|
||||||
val generator = classNameGenerator()
|
val generator = classNameGenerator()
|
||||||
|
val p = xml.patchStrings {
|
||||||
if (!xml.patchStrings {
|
when {
|
||||||
for (i in it.indices) {
|
it.contains(APP_PACKAGE_NAME) -> it.replace(APP_PACKAGE_NAME, pkg)
|
||||||
val s = it[i]
|
it.contains(PLACEHOLDER) -> generator.next()
|
||||||
if (s.contains(APP_PACKAGE_NAME)) {
|
it == origLabel -> label.toString()
|
||||||
it[i] = s.replace(APP_PACKAGE_NAME, pkg)
|
else -> it
|
||||||
} else if (s.contains(PLACEHOLDER)) {
|
|
||||||
it[i] = generator.next()
|
|
||||||
} else if (s == origLabel) {
|
|
||||||
it[i] = label.toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
if (!p) return false
|
||||||
|
|
||||||
// Write apk changes
|
// Write apk changes
|
||||||
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||||
@@ -161,52 +154,87 @@ object AppMigration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchApp(activity: Activity, pkg: String) {
|
private fun patchTest(apk: File, out: File, pkg: String): Boolean {
|
||||||
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
try {
|
||||||
|
JarMap.open(apk, true).use { jar ->
|
||||||
|
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||||
|
val xml = AXML(jar.getRawData(je))
|
||||||
|
val p = xml.patchStrings {
|
||||||
|
when (it) {
|
||||||
|
APP_PACKAGE_NAME -> pkg
|
||||||
|
TEST_PKG_NAME -> "$pkg.test"
|
||||||
|
else -> it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!p) return false
|
||||||
|
|
||||||
|
// Write apk changes
|
||||||
|
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||||
|
val keys = Keygen()
|
||||||
|
out.outputStream().use { SignApk.sign(keys.cert, keys.key, jar, it) }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchApp(context: Context, pkg: String) {
|
||||||
|
val intent = context.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||||
intent.putExtra(Const.Key.PREV_CONFIG, Config.toBundle())
|
intent.putExtra(Const.Key.PREV_CONFIG, Config.toBundle())
|
||||||
val options = ActivityOptions.makeBasic()
|
val options = ActivityOptions.makeBasic()
|
||||||
if (Build.VERSION.SDK_INT >= 34) {
|
if (Build.VERSION.SDK_INT >= 34) {
|
||||||
options.setShareIdentityEnabled(true)
|
options.setShareIdentityEnabled(true)
|
||||||
}
|
}
|
||||||
activity.startActivity(intent, options.toBundle())
|
context.startActivity(intent, options.toBundle())
|
||||||
activity.finish()
|
if (context is Activity) {
|
||||||
|
context.finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
suspend fun patchAndHide(context: Context, label: String, pkg: String? = null): Boolean {
|
||||||
val stub = File(activity.cacheDir, "stub.apk")
|
val stub = File(context.cacheDir, "stub.apk")
|
||||||
try {
|
try {
|
||||||
activity.assets.open("stub.apk").writeTo(stub)
|
context.assets.open("stub.apk").writeTo(stub)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new random package name and signature
|
// Generate a new random signature and package name if needed
|
||||||
val repack = File(activity.cacheDir, "patched.apk")
|
val pkg = pkg ?: genPackageName()
|
||||||
val pkg = genPackageName()
|
|
||||||
Config.keyStoreRaw = ""
|
Config.keyStoreRaw = ""
|
||||||
|
|
||||||
if (!patch(activity, stub, FileOutputStream(repack), pkg, label))
|
// Check and patch the test APK
|
||||||
return false
|
try {
|
||||||
|
val info = context.packageManager.getApplicationInfo(TEST_PKG_NAME, 0)
|
||||||
|
val testApk = File(info.sourceDir)
|
||||||
|
val testRepack = File(context.cacheDir, "test.apk")
|
||||||
|
if (!patchTest(testApk, testRepack, pkg))
|
||||||
|
return false
|
||||||
|
val cmd = "adb_pm_install $testRepack $pkg.test"
|
||||||
|
if (!Shell.cmd(cmd).exec().isSuccess)
|
||||||
|
return false
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
}
|
||||||
|
|
||||||
|
val repack = File(context.cacheDir, "patched.apk")
|
||||||
|
repack.outputStream().use {
|
||||||
|
if (!patch(context, stub, it, pkg, label))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Install and auto launch app
|
// Install and auto launch app
|
||||||
val session = APKInstall.startSession(activity, pkg, onFailure) {
|
val cmd = "adb_pm_install $repack $pkg"
|
||||||
|
if (Shell.cmd(cmd).exec().isSuccess) {
|
||||||
Config.suManager = pkg
|
Config.suManager = pkg
|
||||||
Shell.cmd("touch $AppApkPath").exec()
|
Shell.cmd("touch $AppApkPath").exec()
|
||||||
launchApp(activity, pkg)
|
launchApp(context, pkg)
|
||||||
}
|
return true
|
||||||
|
} else {
|
||||||
val cmd = "adb_pm_install $repack $pkg"
|
|
||||||
if (Shell.cmd(cmd).exec().isSuccess) return true
|
|
||||||
|
|
||||||
try {
|
|
||||||
repack.inputStream().copyAndClose(session.openStream(activity))
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
session.waitIntent()?.let { activity.startActivity(it) } ?: return false
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@@ -217,14 +245,25 @@ object AppMigration {
|
|||||||
setCancelable(false)
|
setCancelable(false)
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
val onFailure = Runnable {
|
val success = withContext(Dispatchers.IO) {
|
||||||
|
patchAndHide(activity, label)
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
val success = withContext(Dispatchers.IO) {
|
}
|
||||||
patchAndHide(activity, label, onFailure)
|
|
||||||
|
suspend fun restoreApp(context: Context): Boolean {
|
||||||
|
val apk = StubApk.current(context)
|
||||||
|
val cmd = "adb_pm_install $apk $APP_PACKAGE_NAME"
|
||||||
|
if (Shell.cmd(cmd).await().isSuccess) {
|
||||||
|
Config.suManager = ""
|
||||||
|
Shell.cmd("touch $AppApkPath").exec()
|
||||||
|
launchApp(context, APP_PACKAGE_NAME)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
if (!success) onFailure.run()
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@@ -235,30 +274,10 @@ object AppMigration {
|
|||||||
setCancelable(false)
|
setCancelable(false)
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
val onFailure = Runnable {
|
if (!restoreApp(activity)) {
|
||||||
dialog.dismiss()
|
|
||||||
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
val apk = StubApk.current(activity)
|
dialog.dismiss()
|
||||||
val session = APKInstall.startSession(activity, APP_PACKAGE_NAME, onFailure) {
|
|
||||||
Config.suManager = ""
|
|
||||||
Shell.cmd("touch $AppApkPath").exec()
|
|
||||||
launchApp(activity, APP_PACKAGE_NAME)
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
val cmd = "adb_pm_install $apk $APP_PACKAGE_NAME"
|
|
||||||
if (Shell.cmd(cmd).await().isSuccess) return
|
|
||||||
val success = withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
apk.inputStream().copyAndClose(session.openStream(activity))
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
return@withContext false
|
|
||||||
}
|
|
||||||
session.waitIntent()?.let { activity.startActivity(it) } ?: return@withContext false
|
|
||||||
return@withContext true
|
|
||||||
}
|
|
||||||
if (!success) onFailure.run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun upgradeStub(context: Context, apk: File): Intent? {
|
suspend fun upgradeStub(context: Context, apk: File): Intent? {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import com.topjohnwu.magisk.core.Const
|
|||||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.unzip
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -47,20 +46,14 @@ open class FlashZip(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isValid = try {
|
try {
|
||||||
zipFile.unzip(installDir, "META-INF/com/google/android", true)
|
val binary = File(installDir, "update-binary")
|
||||||
val script = File(installDir, "updater-script")
|
AppContext.assets.open("module_installer.sh").use { it.writeTo(binary) }
|
||||||
script.readText().contains("#MAGISK")
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Unzip error")
|
console.add("! Unzip error")
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
console.add("! This zip is not a Magisk module!")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
console.add("- Installing ${mUri.displayName}")
|
console.add("- Installing ${mUri.displayName}")
|
||||||
|
|
||||||
return Shell.cmd("sh $installDir/update-binary dummy 1 \'$zipFile\'")
|
return Shell.cmd("sh $installDir/update-binary dummy 1 \'$zipFile\'")
|
||||||
|
|||||||
@@ -597,7 +597,9 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
if (result)
|
if (result)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
Shell.cmd("rm -rf $installDir").submit()
|
// Not every operation initializes installDir
|
||||||
|
if (::installDir.isInitialized)
|
||||||
|
Shell.cmd("rm -rf $installDir").submit()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class AXML(b: ByteArray) {
|
|||||||
* Followed by an array of uint32_t with size = number of strings
|
* Followed by an array of uint32_t with size = number of strings
|
||||||
* Each entry points to an offset into the string data
|
* Each entry points to an offset into the string data
|
||||||
*/
|
*/
|
||||||
fun patchStrings(patchFn: (Array<String>) -> Unit): Boolean {
|
fun patchStrings(mapFn: (String) -> String): Boolean {
|
||||||
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
||||||
|
|
||||||
fun findStringPool(): Int {
|
fun findStringPool(): Int {
|
||||||
@@ -65,7 +65,9 @@ class AXML(b: ByteArray) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val strArr = strList.toTypedArray()
|
val strArr = strList.toTypedArray()
|
||||||
patchFn(strArr)
|
for (i in strArr.indices) {
|
||||||
|
strArr[i] = mapFn(strArr[i])
|
||||||
|
}
|
||||||
|
|
||||||
// Write everything before string data, will patch values later
|
// Write everything before string data, will patch values later
|
||||||
val baos = RawByteStream()
|
val baos = RawByteStream()
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package com.topjohnwu.magisk.core.utils;
|
|||||||
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipUtil;
|
||||||
|
|
||||||
import java.nio.file.attribute.FileTime;
|
import java.nio.file.attribute.FileTime;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
@@ -29,4 +33,15 @@ public class Desugar {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Within {@link ZipArchiveOutputStream#copyFromZipInputStream}, we redirect the method call
|
||||||
|
* {@link ZipUtil#checkRequestedFeatures} to this method. This is safe because the only usage
|
||||||
|
* of copyFromZipInputStream is in {@link ZipArchiveOutputStream#addRawArchiveEntry},
|
||||||
|
* which does not need to actually understand the content of the zip entry. By removing
|
||||||
|
* this feature check, we can modify zip files using unsupported compression methods.
|
||||||
|
*/
|
||||||
|
public static void checkRequestedFeatures(final ZipArchiveEntry ze) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ import android.net.NetworkRequest
|
|||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.collection.ArraySet
|
import androidx.collection.ArraySet
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.ktx.registerRuntimeReceiver
|
import com.topjohnwu.magisk.core.ktx.registerRuntimeReceiver
|
||||||
|
|
||||||
class NetworkObserver(context: Context): DefaultLifecycleObserver {
|
class NetworkObserver(context: Context) {
|
||||||
private val manager = context.getSystemService<ConnectivityManager>()!!
|
private val manager = context.getSystemService<ConnectivityManager>()!!
|
||||||
|
|
||||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
@@ -55,16 +52,13 @@ class NetworkObserver(context: Context): DefaultLifecycleObserver {
|
|||||||
manager.registerNetworkCallback(request, networkCallback)
|
manager.registerNetworkCallback(request, networkCallback)
|
||||||
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
||||||
context.applicationContext.registerRuntimeReceiver(receiver, filter)
|
context.applicationContext.registerRuntimeReceiver(receiver, filter)
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
fun postCurrentState() {
|
||||||
postCurrentState()
|
postValue(
|
||||||
}
|
manager.getNetworkCapabilities(manager.activeNetwork)
|
||||||
|
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true
|
||||||
private fun postCurrentState() {
|
)
|
||||||
postValue(manager.getNetworkCapabilities(manager.activeNetwork)
|
|
||||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun postValue(b: Boolean) {
|
private fun postValue(b: Boolean) {
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.utils;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.lifecycle.LifecycleDispatcher;
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
|
||||||
|
|
||||||
// Use Java to bypass Kotlin internal visibility modifier
|
|
||||||
public class ProcessLifecycle {
|
|
||||||
public static void init(@NonNull Context context) {
|
|
||||||
LifecycleDispatcher.init(context);
|
|
||||||
ProcessLifecycleOwner.init$lifecycle_process_release(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.topjohnwu.magisk.test
|
||||||
|
|
||||||
|
import android.os.ParcelFileDescriptor.AutoCloseInputStream
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.uiautomator.By
|
||||||
|
import androidx.test.uiautomator.Until
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assume.assumeTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class AdditionalTest : BaseTest {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SHELL_PKG = "com.android.shell"
|
||||||
|
private const val LSPOSED_CATEGORY = "org.lsposed.manager.LAUNCH_MANAGER"
|
||||||
|
private const val LSPOSED_PKG = "org.lsposed.manager"
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun teardown() {
|
||||||
|
device.pressHome()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLaunchLsposedManager() {
|
||||||
|
assumeTrue(Environment.lsposed())
|
||||||
|
|
||||||
|
uiAutomation.executeShellCommand(
|
||||||
|
"am start -c $LSPOSED_CATEGORY $SHELL_PKG/.BugreportWarningActivity"
|
||||||
|
).let { pfd -> AutoCloseInputStream(pfd).use { it.readBytes() } }
|
||||||
|
|
||||||
|
val pattern = Pattern.compile("$LSPOSED_PKG:id/.*")
|
||||||
|
assertNotNull(
|
||||||
|
"LSPosed manager launch failed",
|
||||||
|
device.wait(Until.hasObject(By.res(pattern)), TimeUnit.SECONDS.toMillis(10))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/core/src/main/java/com/topjohnwu/magisk/test/BaseTest.kt
Normal file
26
app/core/src/main/java/com/topjohnwu/magisk/test/BaseTest.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package com.topjohnwu.magisk.test
|
||||||
|
|
||||||
|
import android.app.Instrumentation
|
||||||
|
import android.app.UiAutomation
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.uiautomator.UiDevice
|
||||||
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
|
||||||
|
interface BaseTest {
|
||||||
|
val instrumentation: Instrumentation
|
||||||
|
get() = InstrumentationRegistry.getInstrumentation()
|
||||||
|
val context: Context get() = instrumentation.targetContext
|
||||||
|
val uiAutomation: UiAutomation get() = instrumentation.uiAutomation
|
||||||
|
val device: UiDevice get() = UiDevice.getInstance(instrumentation)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun prerequisite() {
|
||||||
|
assertTrue("Should have root access", Shell.getShell().isRoot)
|
||||||
|
// Make sure the root service is running
|
||||||
|
RootUtils.Connection.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user