1
mirror of https://github.com/topjohnwu/Magisk synced 2025-11-03 15:52:30 +01:00

Compare commits

..

71 Commits

Author SHA1 Message Date
topjohnwu
b62835cbeb Release new canary build 2025-01-31 02:36:58 +08:00
Wang Han
6ea740b5ab Skip clearing install dir if not needed 2025-01-27 12:14:55 +08:00
topjohnwu
7ab98dd5ac Make ioctl not a special token 2025-01-27 03:01:00 +08:00
anonymix007
fc8b3400fc Fix sterm parsing logic for ioctl 2025-01-27 03:01:00 +08:00
topjohnwu
54428ba415 Fix Android Studio C++ indexing 2025-01-26 02:24:35 +08:00
topjohnwu
95d1e69d8e Update to ONDK r28.2 2025-01-21 18:50:12 +08:00
topjohnwu
a0f13ab49f Move lambda to static function 2025-01-19 18:59:17 +08:00
topjohnwu
c3e8405020 Update libcxx 2025-01-19 18:51:17 +08:00
Pesh4waaa
a93593ea66 Kurdish Language For Magisk 2025-01-19 15:24:03 +08:00
Wang Han
23eff70883 Fix repeated binding of first argument
Co-authored-by: LoveSy <shana@zju.edu.cn>
2025-01-19 11:57:09 +08:00
vvb2060
110dd4a8b9 Update dependencies 2025-01-19 11:55:44 +08:00
Wang Han
d9c2bffc9f Avoid hardcoding max fd size
Android changed max fd limit to 32768 since Android 9:
cb5fccc83c

Co-authored-by: LoveSy <shana@zju.edu.cn>
2025-01-19 11:54:26 +08:00
topjohnwu
049db49dc8 Use preprocessor for 64bit detection 2025-01-11 00:15:10 +08:00
Wang Han
7c1d2ec61e zygisk: Let client send arch info 2025-01-11 00:15:10 +08:00
topjohnwu
a1b2830c06 Address clippy warnings 2025-01-11 00:11:48 +08:00
topjohnwu
82d1d19267 Migrate uid_granted_root to Rust 2025-01-11 00:11:48 +08:00
topjohnwu
4d4195c02d Migrate prune_su_access to Rust 2025-01-11 00:11:48 +08:00
topjohnwu
5637a258fc Migrate package detection to Rust 2025-01-11 00:11:48 +08:00
topjohnwu
ee6810f417 Rewrite magisk logging implementation 2025-01-11 00:11:48 +08:00
topjohnwu
7098248c64 Add more functionality into Rust 2025-01-11 00:11:48 +08:00
topjohnwu
0d31d356ef Use SQLite's internal time function 2025-01-05 05:04:04 -08:00
topjohnwu
b782e7dcb7 Fetch policy table from Rust 2025-01-05 05:04:04 -08:00
Softastur
a4671b4698 Update Asturian translations
Fixing and updating
2025-01-05 03:15:31 -08:00
topjohnwu
7edd8be169 Minor changes 2025-01-04 02:24:08 -08:00
topjohnwu
24650eefe4 Bind SQLite to Rust 2025-01-04 02:24:08 -08:00
topjohnwu
8e1a44e7eb Use argument binding for query 2025-01-04 02:24:08 -08:00
topjohnwu
2722875190 Use better C++ SQL APIs 2025-01-04 02:24:08 -08:00
topjohnwu
3ca6d06f69 Cleanup database code 2025-01-04 02:24:08 -08:00
topjohnwu
10e47248de Use finer grain sqlite3 APIs 2025-01-04 02:24:08 -08:00
Pzqqt
e73ff679ac scripts: flash_script.sh: Avoid overly dangerous code 2024-12-27 16:02:24 -08:00
Wang Han
53e401fa2d Perform authentication if needed for AutomaticResponse setting 2024-12-27 16:00:02 -08:00
LoveSy
d2768357da Support systemlessly deleting files or folders
After we refactor the magic mount and always mount folder as tmpfs,
we can easily support deleting files or folders now. We recognize
dummy devices with major number 0 and minor number 0 as an indicator
for removing files and folders. This indicator is borrowed from
overlayfs.
2024-12-27 15:57:54 -08:00
LoveSy
a6c2ba7c1e Allow kernel to relabel 2024-12-27 12:35:29 -08:00
LoveSy
aae5b466fb Use rust to implement collect/reset overlay context 2024-12-27 12:35:29 -08:00
5ec1cff
2b7be8b949 init: reset overlay.d files context after sepolicy loaded 2024-12-27 12:35:29 -08:00
5ec1cff
b6511a510d Revert "Allow all domains to access tmpfs files"
This reverts commit da43ac89a0.
2024-12-27 12:35:29 -08:00
Wang Han
704541aef2 Use /metadata/watchdog as preinit dir if exists
Since Android 15, all domains are allowed to search /metadata so preinit
dir will be exposed. Use /metadata/watchdog when /metadata is chosen as
preinit device, and the dir is available (since Android 11).
2024-12-27 10:35:05 -08:00
topjohnwu
005560a4c5 Always rescan manager APK when database is updated 2024-12-26 12:18:38 -08:00
topjohnwu
231a5d1853 Cleanup test code 2024-12-25 22:26:30 -08:00
topjohnwu
9e2b59060d Drive app migration tests through instrumentation
Make tests less flaky
2024-12-25 22:26:30 -08:00
topjohnwu
08ea937f7c Test su request via instrumentation 2024-12-25 22:26:30 -08:00
topjohnwu
2baedf74d1 Install and test LSPosed through test app 2024-12-25 22:26:30 -08:00
topjohnwu
32faa4ced6 Redesign test APK architecture
The test APK and the main APK share the same process and classloader,
and in the non-hidden case, the test APK's classes take precedence over
the ones in the main APK. This causes issues because the test APK and
main APK share some dependencies, but don't always use the same
version. This is especially problematic for the Kotlin stdlib and
AndroidX dependencies.

The solution here is to rely on R8's obfuscation feature and repackage
all potentially conflicting classes into a separate package in the test
APK. To ensure that the test classes are always using the same classes
as the main APK, we have to directly implement all tests inside the main
APK, making the test APK purely a "test runner with test dependencies".

As a result, the test APK can only be used when built in release mode,
because R8 no longer allow class obfuscation to be enabled when building
for debug versions.
2024-12-25 20:17:57 -08:00
topjohnwu
ccdb0b5d13 Add ability to skip certain test variants 2024-12-25 20:11:21 -08:00
topjohnwu
8506b672ad Update CI operating system 2024-12-23 22:52:30 -08:00
topjohnwu
ce2e33bb20 Cleanup test scripts 2024-12-23 20:42:54 -08:00
topjohnwu
6707b72260 Fix AVD tests 2024-12-23 20:41:42 -08:00
topjohnwu
5885b8c20d Add new tests for app hiding 2024-12-18 17:22:31 -08:00
topjohnwu
820710c086 Fix incorrect SQLite syntax 2024-12-18 17:22:31 -08:00
topjohnwu
51cf196bf7 Always use root to hide/restore app 2024-12-18 17:22:31 -08:00
Wang Han
dadba44cf9 Update module installer guide about META-INF 2024-12-17 16:36:40 -08:00
topjohnwu
2ce4a5543b Make ndk-build happy when Rust libs are missing 2024-12-13 17:00:40 -08:00
topjohnwu
9112a3a4f5 Introduce instrumentation tests 2024-12-13 12:07:42 -08:00
topjohnwu
24615afda1 Remove usage of ProcessLifecycle 2024-12-13 12:07:42 -08:00
topjohnwu
c5778f398b Cleanup imports 2024-12-13 12:07:42 -08:00
topjohnwu
4eb4097b9b Split file processing into its own class 2024-12-13 12:07:42 -08:00
vvb2060
c512496847 install_module: simplify script 2024-12-12 10:08:09 -08:00
vvb2060
506961a10d flash module: ignore META-INF 2024-12-12 10:07:47 -08:00
topjohnwu
3414415907 Support zip files with unsupported compresssion method 2024-12-12 02:50:19 -08:00
topjohnwu
dc2ae7cfd1 Disable CI on master push
Changes should be done through PRs for CI
2024-12-12 02:50:19 -08:00
topjohnwu
2e86d21c29 16k pages only work on Android B on x64 2024-12-09 20:13:27 -08:00
topjohnwu
2654382c43 Address clippy warnings 2024-12-09 18:26:39 -08:00
topjohnwu
9e26b73813 Update rust dependencies 2024-12-09 18:26:39 -08:00
topjohnwu
10cd13bf80 Update ONDK r28.1 2024-12-09 18:26:39 -08:00
topjohnwu
f10ee5f887 Test 16K pages with AVD instead of Cuttlefish 2024-12-09 14:16:08 -08:00
topjohnwu
47cc532d96 Release new canary build 2024-12-06 18:19:06 -08:00
topjohnwu
218327f92b Release Magisk v28.1 2024-12-06 17:45:41 -08:00
topjohnwu
4eae66a1a7 Add v28.1 release notes 2024-12-06 17:38:43 -08:00
vvb2060
b09ceeb43c scripts: sync avd_magisk.sh with mgiskinit 2024-12-06 02:21:17 -08:00
vvb2060
4fb539c110 core: use a new tmpfs as worker 2024-12-06 02:19:43 -08:00
vvb2060
849b284da5 core: insert symlink magisk_node 2024-12-06 02:19:32 -08:00
110 changed files with 3642 additions and 2117 deletions

View File

@@ -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

View File

@@ -20,9 +20,9 @@ Some highlight features:
Click the icon below to download Magisk apk. Click the icon below to download Magisk apk.
[![](https://img.shields.io/badge/Magisk-v27.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v27.0) [![](https://img.shields.io/badge/Magisk-v28.1-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v28.1)
[![](https://img.shields.io/badge/Magisk%20Beta-v28.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v28.0) [![](https://img.shields.io/badge/Magisk%20Beta-v28.1-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v28.1)
[![](https://img.shields.io/badge/Magisk-Canary-red)](https://github.com/topjohnwu/Magisk/releases/tag/canary-28003) [![](https://img.shields.io/badge/Magisk-Canary-red)](https://github.com/topjohnwu/Magisk/releases/tag/canary-28102)
## Useful Links ## Useful Links

View File

@@ -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()
} }
} }

View File

@@ -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)
} }

View File

@@ -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

View File

@@ -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) {}

View File

@@ -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 =

View File

@@ -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)

View File

@@ -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
} }
} }
} }

View File

@@ -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

View File

@@ -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\""
} }
} }

View File

@@ -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

View File

@@ -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
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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()
}
}
}

View File

@@ -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 = {})
}

View File

@@ -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)!!

View File

@@ -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(

View File

@@ -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,

View File

@@ -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
)
}
} }

View File

@@ -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)
} }
} }

View File

@@ -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)
}
}
}

View File

@@ -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? {

View File

@@ -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\'")

View File

@@ -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
} }

View File

@@ -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()

View File

@@ -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
}
} }

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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))
)
}
}

View 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