Compare commits

..

No commits in common. "master" and "v28.1" have entirely different histories.

276 changed files with 10427 additions and 14037 deletions

6
.gitattributes vendored
View File

@ -12,11 +12,13 @@
# Denote all files that are truly binary and should not be modified. # Denote all files that are truly binary and should not be modified.
tools/** binary tools/** binary
tools/rustup-wrapper/** -binary
tools/elf-cleaner/** -binary
*.jar binary *.jar binary
*.exe binary *.exe binary
*.apk binary *.apk binary
*.png binary *.png binary
*.jpg binary *.jpg binary
*.ttf binary *.ttf binary
# Help GitHub detect languages
native/jni/external/** linguist-vendored
native/jni/systemproperties/** linguist-language=C++

View File

@ -26,15 +26,6 @@ runs:
- name: Cache sccache - name: Cache sccache
uses: actions/cache@v4 uses: actions/cache@v4
if: ${{ github.event_name != 'pull_request' }}
with:
path: .sccache
key: sccache-${{ runner.os }}-${{ github.sha }}
restore-keys: sccache-${{ runner.os }}-
- name: Restore sccache
uses: actions/cache/restore@v4
if: ${{ github.event_name == 'pull_request' }}
with: with:
path: .sccache path: .sccache
key: sccache-${{ runner.os }}-${{ github.sha }} key: sccache-${{ runner.os }}-${{ github.sha }}
@ -64,7 +55,7 @@ runs:
- name: Cache Gradle dependencies - name: Cache Gradle dependencies
uses: actions/cache@v4 uses: actions/cache@v4
if: ${{ inputs.is-asset-build == 'true' && github.event_name != 'pull_request' }} if: inputs.is-asset-build == 'true'
with: with:
path: | path: |
.gradle/caches .gradle/caches
@ -75,7 +66,7 @@ runs:
- name: Restore Gradle dependencies - name: Restore Gradle dependencies
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
if: ${{ inputs.is-asset-build == 'false' || github.event_name == 'pull_request' }} if: inputs.is-asset-build == 'false'
with: with:
path: | path: |
.gradle/caches .gradle/caches
@ -87,17 +78,19 @@ runs:
- name: Cache Gradle build cache - name: Cache Gradle build cache
uses: actions/cache@v4 uses: actions/cache@v4
if: ${{ inputs.is-asset-build == 'true' && github.event_name != 'pull_request' }} if: inputs.is-asset-build == 'true'
with: with:
path: .gradle/caches/build-cache-* path: |
.gradle/caches/build-cache-*
key: gradle-build-cache-${{ github.sha }} key: gradle-build-cache-${{ github.sha }}
restore-keys: gradle-build-cache- restore-keys: gradle-build-cache-
- name: Restore Gradle build cache - name: Restore Gradle build cache
uses: actions/cache/restore@v4 uses: actions/cache/restore@v4
if: ${{ inputs.is-asset-build == 'false' || github.event_name == 'pull_request' }} if: inputs.is-asset-build == 'false'
with: with:
path: .gradle/caches/build-cache-* path: |
.gradle/caches/build-cache-*
key: gradle-build-cache-${{ github.sha }} key: gradle-build-cache-${{ github.sha }}
restore-keys: gradle-build-cache- restore-keys: gradle-build-cache-
enableCrossOsArchive: true enableCrossOsArchive: true

View File

@ -6,7 +6,9 @@ on:
paths: paths:
- "app/**" - "app/**"
- "native/**" - "native/**"
- "buildSrc/**"
- "build.py" - "build.py"
- "gradle.properties"
- ".github/workflows/build.yml" - ".github/workflows/build.yml"
pull_request: pull_request:
branches: [master] branches: [master]
@ -15,7 +17,7 @@ on:
jobs: jobs:
build: build:
name: Build Magisk artifacts name: Build Magisk artifacts
runs-on: macos-15 runs-on: macos-14
strategy: strategy:
fail-fast: false fail-fast: false
steps: steps:
@ -36,7 +38,7 @@ jobs:
run: ./build.py -v all run: ./build.py -v all
- name: Stop gradle daemon - name: Stop gradle daemon
run: ./app/gradlew --stop run: ./gradlew --stop
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@ -58,7 +60,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [windows-2025, ubuntu-24.04] os: [windows-latest, ubuntu-latest]
steps: steps:
- name: Check out - name: Check out
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -72,23 +74,20 @@ jobs:
run: python build.py -v -c .github/ci.prop all run: python build.py -v -c .github/ci.prop all
- name: Stop gradle daemon - name: Stop gradle daemon
run: ./app/gradlew --stop run: ./gradlew --stop
avd-test: avd-test:
name: Test API ${{ matrix.version }} (x86_64) name: Test API ${{ matrix.version }} (x86_64)
runs-on: ubuntu-24.04 runs-on: ubuntu-latest
needs: build needs: build
if: ${{ github.event_name != 'push' }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
version: [23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35] version: [23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35]
type: [""] type: [""]
include: include:
- version: 36 - version: "Baklava"
type: "google_apis" type: "google_apis"
- version: 36
type: "google_apis_ps16k"
steps: steps:
- name: Check out - name: Check out
@ -110,7 +109,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
env: env:
AVD_TEST_LOG: 1 AVD_TEST_LOG: 1
run: scripts/avd.sh test ${{ matrix.version }} ${{ matrix.type }} run: scripts/avd_test.sh ${{ matrix.version }} ${{ matrix.type }}
- name: Upload logs on error - name: Upload logs on error
if: ${{ failure() }} if: ${{ failure() }}
@ -123,9 +122,8 @@ jobs:
avd-test-32: avd-test-32:
name: Test API ${{ matrix.version }} (x86) name: Test API ${{ matrix.version }} (x86)
runs-on: ubuntu-24.04 runs-on: ubuntu-latest
needs: build needs: build
if: ${{ github.event_name != 'push' }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -152,7 +150,7 @@ jobs:
env: env:
FORCE_32_BIT: 1 FORCE_32_BIT: 1
AVD_TEST_LOG: 1 AVD_TEST_LOG: 1
run: scripts/avd.sh test ${{ matrix.version }} run: scripts/avd_test.sh ${{ matrix.version }}
- name: Upload logs on error - name: Upload logs on error
if: ${{ failure() }} if: ${{ failure() }}
@ -163,19 +161,20 @@ 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
if: ${{ github.event_name != 'push' }}
env: env:
CF_HOME: /home/runner/aosp_cf_phone CF_HOME: /home/runner/aosp_cf_phone
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- branch: "aosp-android-latest-release" - branch: "aosp-main"
device: "aosp_cf_x86_64_only_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
@ -194,7 +193,7 @@ jobs:
- name: Run Cuttlefish test - name: Run Cuttlefish test
timeout-minutes: 10 timeout-minutes: 10
run: sudo -E -u $USER scripts/cuttlefish.sh test run: su $USER -c 'scripts/cuttlefish.sh test'
- name: Upload logs on error - name: Upload logs on error
if: ${{ failure() }} if: ${{ failure() }}

10
.gitignore vendored
View File

@ -2,13 +2,19 @@ out
*.zip *.zip
*.jks *.jks
*.apk *.apk
*.log
/config.prop /config.prop
/notes.md /notes.md
/update.sh
/app/dict.txt
# Built binaries # Built binaries
native/out native/out
# Android Studio # Android Studio / Gradle
*.iml *.iml
.gradle
.idea .idea
.kotlin
/local.properties
/build
/captures

12
.gitmodules vendored
View File

@ -4,12 +4,21 @@
[submodule "lz4"] [submodule "lz4"]
path = native/src/external/lz4 path = native/src/external/lz4
url = https://github.com/lz4/lz4.git url = https://github.com/lz4/lz4.git
[submodule "bzip2"]
path = native/src/external/bzip2
url = https://github.com/nemequ/bzip2.git
[submodule "xz"] [submodule "xz"]
path = native/src/external/xz path = native/src/external/xz
url = https://github.com/xz-mirror/xz.git url = https://github.com/xz-mirror/xz.git
[submodule "libcxx"] [submodule "libcxx"]
path = native/src/external/libcxx path = native/src/external/libcxx
url = https://github.com/topjohnwu/libcxx.git url = https://github.com/topjohnwu/libcxx.git
[submodule "zlib"]
path = native/src/external/zlib
url = https://android.googlesource.com/platform/external/zlib
[submodule "zopfli"]
path = native/src/external/zopfli
url = https://github.com/google/zopfli.git
[submodule "cxx-rs"] [submodule "cxx-rs"]
path = native/src/external/cxx-rs path = native/src/external/cxx-rs
url = https://github.com/topjohnwu/cxx.git url = https://github.com/topjohnwu/cxx.git
@ -22,3 +31,6 @@
[submodule "crt0"] [submodule "crt0"]
path = native/src/external/crt0 path = native/src/external/crt0
url = https://github.com/topjohnwu/crt0.git url = https://github.com/topjohnwu/crt0.git
[submodule "termux-elf-cleaner"]
path = tools/termux-elf-cleaner
url = https://github.com/termux/termux-elf-cleaner.git

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-v29.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v29.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-v29.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v29.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-29001) [![](https://img.shields.io/badge/Magisk-Canary-red)](https://github.com/topjohnwu/Magisk/releases/tag/canary-28003)
## Useful Links ## Useful Links

7
app/.gitignore vendored
View File

@ -1,7 +0,0 @@
/dict.txt
# Gradle
.gradle
.kotlin
/local.properties
/build

View File

@ -35,7 +35,7 @@ android {
} }
dependencies { dependencies {
implementation(project(":core")) implementation(project(":app:core"))
coreLibraryDesugaring(libs.jdk.libs) coreLibraryDesugaring(libs.jdk.libs)
implementation(libs.indeterminate.checkbox) implementation(libs.indeterminate.checkbox)

View File

@ -1,13 +1,10 @@
package com.topjohnwu.magisk.arch package com.topjohnwu.magisk.arch
import android.content.ContentResolver
import android.view.KeyEvent import android.view.KeyEvent
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navOptions
import com.topjohnwu.magisk.utils.AccessibilityUtils
abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Binding>() { abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Binding>() {
@ -34,17 +31,7 @@ abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Bindin
} }
} }
companion object {
fun navigate(directions: NavDirections, navigation: NavController, cr: ContentResolver) {
if (AccessibilityUtils.isAnimationEnabled(cr)) {
navigation.navigate(directions)
} else {
navigation.navigate(directions, navOptions {})
}
}
}
fun NavDirections.navigate() { fun NavDirections.navigate() {
navigate(this, navigation, contentResolver) navigation.navigate(this)
} }
} }

View File

@ -3,6 +3,7 @@ package com.topjohnwu.magisk.dialog
import com.topjohnwu.magisk.core.AppContext import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R import com.topjohnwu.magisk.core.R
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.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
@ -10,10 +11,15 @@ import java.io.File
class ManagerInstallDialog : MarkDownDialog() { class ManagerInstallDialog : MarkDownDialog() {
private val svc get() = ServiceLocator.networkService
override suspend fun getMarkdownText(): String { override suspend fun getMarkdownText(): String {
val text = Info.update.note val text = svc.fetchString(Info.remote.magisk.note)
// Cache the changelog // Cache the changelog
File(AppContext.cacheDir, "${Info.update.versionCode}.md").writeText(text) AppContext.cacheDir.listFiles { _, name -> name.endsWith(".md") }.orEmpty().forEach {
it.delete()
}
File(AppContext.cacheDir, "${Info.remote.magisk.versionCode}.md").writeText(text)
return text return text
} }

View File

@ -17,8 +17,6 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.DownloadEngine import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
import com.topjohnwu.magisk.core.R as CoreR import com.topjohnwu.magisk.core.R as CoreR
import androidx.navigation.findNavController
import com.topjohnwu.magisk.arch.NavigationActivity
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider { class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
@ -70,13 +68,7 @@ class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
override fun onMenuItemSelected(item: MenuItem): Boolean { override fun onMenuItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_settings -> R.id.action_settings ->
activity?.let { HomeFragmentDirections.actionHomeFragmentToSettingsFragment().navigate()
NavigationActivity.navigate(
HomeFragmentDirections.actionHomeFragmentToSettingsFragment(),
it.findNavController(R.id.main_nav_host),
it.contentResolver,
)
}
R.id.action_reboot -> activity?.let { RebootMenu.inflate(it).show() } R.id.action_reboot -> activity?.let { RebootMenu.inflate(it).show() }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }

View File

@ -91,15 +91,16 @@ class HomeViewModel(
override suspend fun doLoadWork() { override suspend fun doLoadWork() {
appState = State.LOADING appState = State.LOADING
Info.fetchUpdate(svc)?.apply { Info.getRemote(svc)?.apply {
appState = when { appState = when {
BuildConfig.APP_VERSION_CODE < versionCode -> State.OUTDATED BuildConfig.APP_VERSION_CODE < magisk.versionCode -> State.OUTDATED
else -> State.UP_TO_DATE else -> State.UP_TO_DATE
} }
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
managerRemoteVersion = managerRemoteVersion =
("$version (${versionCode})" + if (isDebug) " (D)" else "").asText() ("${magisk.version} (${magisk.versionCode})" +
if (isDebug) " (D)" else "").asText()
} ?: run { } ?: run {
appState = State.INVALID appState = State.INVALID
managerRemoteVersion = CoreR.string.not_available.asText() managerRemoteVersion = CoreR.string.not_available.asText()

View File

@ -41,7 +41,7 @@ object RebootMenu {
activity.getSystemService<PowerManager>()?.isRebootingUserspaceSupported == true) { activity.getSystemService<PowerManager>()?.isRebootingUserspaceSupported == true) {
menu.menu.findItem(R.id.action_reboot_userspace).isVisible = true menu.menu.findItem(R.id.action_reboot_userspace).isVisible = true
} }
if (Const.Version.atLeast_28_0()) { if (Const.Version.isCanary()) {
menu.menu.findItem(R.id.action_reboot_safe_mode).isChecked = Config.bootloop >= 2 menu.menu.findItem(R.id.action_reboot_safe_mode).isChecked = Config.bootloop >= 2
} else { } else {
menu.menu.findItem(R.id.action_reboot_safe_mode).isVisible = false menu.menu.findItem(R.id.action_reboot_safe_mode).isVisible = false

View File

@ -73,9 +73,9 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
val file = File(AppContext.cacheDir, "${BuildConfig.APP_VERSION_CODE}.md") val file = File(AppContext.cacheDir, "${BuildConfig.APP_VERSION_CODE}.md")
val text = when { val text = when {
file.exists() -> file.readText() file.exists() -> file.readText()
Const.Url.CHANGELOG_URL.isEmpty() -> ""
else -> { else -> {
val str = if (Const.APP_IS_CANARY) Info.update.note val str = svc.fetchString(Const.Url.CHANGELOG_URL)
else svc.fetchString(Const.Url.CHANGELOG_URL)
file.writeText(str) file.writeText(str)
str str
} }
@ -100,15 +100,13 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
} }
override fun onSaveState(state: Bundle) { override fun onSaveState(state: Bundle) {
state.putParcelable( state.putParcelable(INSTALL_STATE_KEY, InstallState(
INSTALL_STATE_KEY, InstallState( methodId,
methodId, step,
step, Config.keepVerity,
Config.keepVerity, Config.keepEnc,
Config.keepEnc, Config.recovery
Config.recovery ))
)
)
} }
override fun onRestoreState(state: Bundle) { override fun onRestoreState(state: Bundle) {
@ -126,7 +124,6 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
override fun onActivityLaunch() { override fun onActivityLaunch() {
AppContext.toast(CoreR.string.patch_file_msg, Toast.LENGTH_LONG) AppContext.toast(CoreR.string.patch_file_msg, Toast.LENGTH_LONG)
} }
override fun onActivityResult(result: Uri) { override fun onActivityResult(result: Uri) {
uri.value = result uri.value = result
} }

View File

@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.HorizontalScrollView
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
@ -13,7 +12,6 @@ import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding
import com.topjohnwu.magisk.ui.MainActivity import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.utils.AccessibilityUtils
import com.topjohnwu.magisk.utils.MotionRevealHelper import com.topjohnwu.magisk.utils.MotionRevealHelper
import rikka.recyclerview.addEdgeSpacing import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addItemSpacing import rikka.recyclerview.addItemSpacing
@ -58,11 +56,6 @@ class LogFragment : BaseFragment<FragmentLogMd2Binding>(), MenuProvider {
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1) addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect() fixEdgeEffect()
} }
if (!AccessibilityUtils.isAnimationEnabled(requireContext().contentResolver)) {
val scrollView = view.findViewById<HorizontalScrollView>(R.id.log_scroll_magisk)
scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER)
}
} }

View File

@ -147,7 +147,7 @@ object UpdateChannel : BaseSettingsItem.Selector() {
get() = Config.updateChannel get() = Config.updateChannel
set(value) { set(value) {
Config.updateChannel = value Config.updateChannel = value
Info.resetUpdate() Info.remote = Info.EMPTY_REMOTE
} }
override val title = CoreR.string.settings_update_channel_title.asText() override val title = CoreR.string.settings_update_channel_title.asText()
@ -169,7 +169,7 @@ object UpdateChannelUrl : BaseSettingsItem.Input() {
get() = Config.customChannelUrl get() = Config.customChannelUrl
set(value) { set(value) {
Config.customChannelUrl = value Config.customChannelUrl = value
Info.resetUpdate() Info.remote = Info.EMPTY_REMOTE
notifyPropertyChanged(BR.description) notifyPropertyChanged(BR.description)
} }

View File

@ -13,7 +13,6 @@ 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
@ -93,7 +92,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()
AutomaticResponse -> if (Config.suAuth) AuthEvent(doAction).publish() else doAction() Hide, Restore -> withInstallPermission(doAction)
else -> doAction() else -> doAction()
} }
} }

View File

@ -1,14 +0,0 @@
package com.topjohnwu.magisk.utils
import android.content.ContentResolver
import android.provider.Settings
class AccessibilityUtils {
companion object {
fun isAnimationEnabled(cr: ContentResolver): Boolean {
return !(Settings.Global.getFloat(cr, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0.0f
&& Settings.Global.getFloat(cr, Settings.Global.TRANSITION_ANIMATION_SCALE, 1.0f) == 0.0f
&& Settings.Global.getFloat(cr, Settings.Global.WINDOW_ANIMATION_SCALE, 1.0f) == 0.0f)
}
}
}

View File

@ -16,7 +16,6 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<HorizontalScrollView <HorizontalScrollView
android:id="@+id/log_scroll_magisk"
gone="@{viewModel.loading}" gone="@{viewModel.loading}"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -1,11 +1,5 @@
plugins { tasks.register("clean") {
id("MagiskPlugin")
}
tasks.register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
subprojects.forEach { subprojects.forEach {
dependsOn(":${it.name}:clean") dependsOn(":app:${it.name}:clean")
} }
} }

View File

@ -29,7 +29,7 @@ android {
} }
dependencies { dependencies {
api(project(":shared")) api(project(":app:shared"))
api(libs.timber) api(libs.timber)
api(libs.markwon.core) api(libs.markwon.core)
@ -60,10 +60,5 @@ 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,4 +37,12 @@
-flattenpackagehierarchy -flattenpackagehierarchy
-allowaccessmodification -allowaccessmodification
-dontwarn org.junit.** -dontwarn org.bouncycastle.jsse.BCSSLParameters
-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

@ -14,6 +14,7 @@
<application <application
android:name=".App" android:name=".App"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:multiArch="true"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning" tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
tools:remove="android:appComponentFactory"> tools:remove="android:appComponentFactory">

View File

@ -16,6 +16,7 @@ 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
@ -39,7 +40,6 @@ 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,10 +56,6 @@ 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)
@ -106,7 +102,8 @@ object AppContext : ContextWrapper(null),
val lm = getSystemService(LocaleManager::class.java) val lm = getSystemService(LocaleManager::class.java)
lm.overrideLocaleConfig = LocaleSetting.localeConfig lm.overrideLocaleConfig = LocaleSetting.localeConfig
} }
networkObserver = NetworkObserver.init(this) ProcessLifecycle.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)
@ -123,6 +120,7 @@ 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 = longArrayOf(0, -1, 10, 20, 30, 60) val TIMEOUT_LIST = intArrayOf(0, -1, 10, 20, 30, 60)
} }
private val defaultChannel = private val defaultChannel =

View File

@ -2,7 +2,6 @@ package com.topjohnwu.magisk.core
import android.os.Build import android.os.Build
import android.os.Process import android.os.Process
import com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
object Const { object Const {
@ -15,13 +14,13 @@ object Const {
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull() else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
// Paths // Paths
const val MODULE_PATH = "/data/adb/modules" const val MAGISK_PATH = "/data/adb/modules"
const val TMPDIR = "/dev/tmp" const val TMPDIR = "/dev/tmp"
const val MAGISK_LOG = "/cache/magisk.log" const val MAGISK_LOG = "/cache/magisk.log"
// Misc // Misc
val USER_ID = Process.myUid() / 100000 val USER_ID = Process.myUid() / 100000
val APP_IS_CANARY get() = Version.isCanary(APP_VERSION_CODE) val APP_IS_CANARY get() = Version.isCanary(BuildConfig.APP_VERSION_CODE)
object Version { object Version {
const val MIN_VERSION = "v22.0" const val MIN_VERSION = "v22.0"
@ -29,7 +28,6 @@ object Const {
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary() fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary() fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
fun atLeast_28_0() = Info.env.versionCode >= 28000 || isCanary()
fun isCanary() = isCanary(Info.env.versionCode) fun isCanary() = isCanary(Info.env.versionCode)
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0 fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
@ -44,11 +42,13 @@ object Const {
const val PATREON_URL = "https://www.patreon.com/topjohnwu" const val PATREON_URL = "https://www.patreon.com/topjohnwu"
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk" const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
const val CHANGELOG_URL = "https://topjohnwu.github.io/Magisk/releases/${APP_VERSION_CODE}.md" val CHANGELOG_URL = if (APP_IS_CANARY) Info.remote.magisk.note
else "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.APP_VERSION_CODE}.md"
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
const val GITHUB_API_URL = "https://api.github.com/" const val GITHUB_API_URL = "https://api.github.com/"
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/" const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/"
const val INVALID_URL = "https://example.com/" const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
} }
object Key { object Key {

View File

@ -19,18 +19,12 @@ object Info {
var stub: StubApk.Data? = null var stub: StubApk.Data? = null
private val EMPTY_UPDATE = UpdateInfo() val EMPTY_REMOTE = UpdateInfo()
var update = EMPTY_UPDATE var remote = EMPTY_REMOTE
private set suspend fun getRemote(svc: NetworkService): UpdateInfo? {
return if (remote === EMPTY_REMOTE) {
suspend fun fetchUpdate(svc: NetworkService): UpdateInfo? { svc.fetchUpdate()?.apply { remote = this }
return if (update === EMPTY_UPDATE) { } else remote
svc.fetchUpdate()?.apply { update = this }
} else update
}
fun resetUpdate() {
update = EMPTY_UPDATE
} }
var isRooted = false var isRooted = false
@ -53,6 +47,7 @@ object Info {
private set private set
private var crypto = "" private var crypto = ""
var hasGMS = true
val isEmulator = val isEmulator =
Build.DEVICE.contains("vsoc") Build.DEVICE.contains("vsoc")
|| getProperty("ro.kernel.qemu", "0") == "1" || getProperty("ro.kernel.qemu", "0") == "1"

View File

@ -11,7 +11,6 @@ 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
@ -26,7 +25,7 @@ class JobService : BaseJobService() {
@TargetApi(value = 34) @TargetApi(value = 34)
inner class Session( inner class Session(
private var params: JobParameters private var params: JobParameters
) : DownloadSession { ) : DownloadEngine.Session {
override val context get() = this@JobService override val context get() = this@JobService
val engine = DownloadEngine(this) val engine = DownloadEngine(this)
@ -75,8 +74,9 @@ class JobService : BaseJobService() {
private fun checkUpdate(params: JobParameters): Boolean { private fun checkUpdate(params: JobParameters): Boolean {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
Info.fetchUpdate(ServiceLocator.networkService)?.let { ServiceLocator.networkService.fetchUpdate()?.let {
if (Info.env.isActive && BuildConfig.APP_VERSION_CODE < it.versionCode) Info.remote = it
if (Info.env.isActive && BuildConfig.APP_VERSION_CODE < it.magisk.versionCode)
Notifications.updateAvailable() Notifications.updateAvailable()
jobFinished(params, false) jobFinished(params, false)
} }

View File

@ -3,6 +3,7 @@ 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() {
@ -12,7 +13,7 @@ class Provider : BaseProvider() {
SuCallbackHandler.run(context!!, method, extras) SuCallbackHandler.run(context!!, method, extras)
Bundle.EMPTY Bundle.EMPTY
} }
else -> Bundle.EMPTY else -> TestHandler.run(method)
} }
} }
} }

View File

@ -7,10 +7,9 @@ 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(), DownloadSession { class Service : BaseService(), DownloadEngine.Session {
private var mEngine: DownloadEngine? = null private var mEngine: DownloadEngine? = null
override val context get() = this override val context get() = this

View File

@ -0,0 +1,45 @@
package com.topjohnwu.magisk.core.data
import com.topjohnwu.magisk.core.model.BranchInfo
import com.topjohnwu.magisk.core.model.ModuleJson
import com.topjohnwu.magisk.core.model.UpdateInfo
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Path
import retrofit2.http.Streaming
import retrofit2.http.Url
private const val BRANCH = "branch"
private const val REPO = "repo"
private const val FILE = "file"
interface GithubPageServices {
@GET
suspend fun fetchUpdateJSON(@Url file: String): UpdateInfo
}
interface RawServices {
@GET
@Streaming
suspend fun fetchFile(@Url url: String): ResponseBody
@GET
suspend fun fetchString(@Url url: String): String
@GET
suspend fun fetchModuleJson(@Url url: String): ModuleJson
}
interface GithubApiServices {
@GET("repos/{$REPO}/branches/{$BRANCH}")
@Headers("Accept: application/vnd.github.v3+json")
suspend fun fetchBranch(
@Path(REPO, encoded = true) repo: String,
@Path(BRANCH) branch: String
): BranchInfo
}

View File

@ -1,48 +0,0 @@
package com.topjohnwu.magisk.core.data
import com.topjohnwu.magisk.core.model.ModuleJson
import com.topjohnwu.magisk.core.model.Release
import com.topjohnwu.magisk.core.model.UpdateJson
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Path
import retrofit2.http.Query
import retrofit2.http.Streaming
import retrofit2.http.Url
interface RawUrl {
@GET
@Streaming
suspend fun fetchFile(@Url url: String): ResponseBody
@GET
suspend fun fetchString(@Url url: String): String
@GET
suspend fun fetchModuleJson(@Url url: String): ModuleJson
@GET
suspend fun fetchUpdateJson(@Url url: String): UpdateJson
}
interface GithubApiServices {
@GET("/repos/{owner}/{repo}/releases")
@Headers("Accept: application/vnd.github+json")
suspend fun fetchReleases(
@Path("owner") owner: String = "topjohnwu",
@Path("repo") repo: String = "Magisk",
@Query("per_page") per: Int = 10,
@Query("page") page: Int = 1,
): Response<MutableList<Release>>
@GET("/repos/{owner}/{repo}/releases/latest")
@Headers("Accept: application/vnd.github+json")
suspend fun fetchLatestRelease(
@Path("owner") owner: String = "topjohnwu",
@Path("repo") repo: String = "Magisk",
): Release
}

View File

@ -7,13 +7,9 @@ import kotlinx.coroutines.withContext
open class MagiskDB { open class MagiskDB {
class Literal( suspend fun <R> exec(
val str: String
)
suspend inline fun <R> exec(
query: String, query: String,
crossinline mapper: (Map<String, String>) -> R mapper: suspend (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
@ -22,15 +18,13 @@ 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) .let { mapper(it) }
} }
} }
} }
suspend fun exec(query: String) { suspend inline fun exec(query: String) {
withContext(Dispatchers.IO) { exec(query) {}
Shell.cmd("magisk --sqlite '$query'").await()
}
} }
fun Map<String, Any>.toQuery(): String { fun Map<String, Any>.toQuery(): String {
@ -39,7 +33,6 @@ 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 < strftime(\"%s\", \"now\")) OR until < 0" "(until > 0 AND until < $nowSeconds) 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_QUERY FROM ${Table.POLICY} WHERE uid=$uid LIMIT 1" val query = "SELECT * 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_QUERY FROM ${Table.POLICY} WHERE uid/100000=${Const.USER_ID}" val query = "SELECT * FROM ${Table.POLICY} WHERE uid/100000 == ${Const.USER_ID}"
return exec(query, ::toPolicy).filterNotNull() return exec(query, ::toPolicy).filterNotNull()
} }
@ -43,15 +43,8 @@ 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

@ -5,7 +5,7 @@ import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.ProviderInstaller import com.topjohnwu.magisk.ProviderInstaller
import com.topjohnwu.magisk.core.BuildConfig import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.DateTimeAdapter import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.utils.LocaleSetting import com.topjohnwu.magisk.core.utils.LocaleSetting
import okhttp3.Cache import okhttp3.Cache
import okhttp3.ConnectionSpec import okhttp3.ConnectionSpec
@ -72,13 +72,15 @@ fun createOkHttpClient(context: Context): OkHttpClient {
chain.proceed(request.build()) chain.proceed(request.build())
} }
ProviderInstaller.install(context) if (!ProviderInstaller.install(context)) {
Info.hasGMS = false
}
return builder.build() return builder.build()
} }
fun createMoshiConverterFactory(): MoshiConverterFactory { fun createMoshiConverterFactory(): MoshiConverterFactory {
val moshi = Moshi.Builder().add(DateTimeAdapter()).build() val moshi = Moshi.Builder().build()
return MoshiConverterFactory.create(moshi) return MoshiConverterFactory.create(moshi)
} }

View File

@ -35,8 +35,8 @@ object ServiceLocator {
val markwon by lazy { createMarkwon(AppContext) } val markwon by lazy { createMarkwon(AppContext) }
val networkService by lazy { val networkService by lazy {
NetworkService( NetworkService(
createApiService(retrofit, Const.Url.INVALID_URL), createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
createApiService(retrofit, Const.Url.GITHUB_API_URL), createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
) )
} }
} }

View File

@ -7,6 +7,7 @@ import android.app.PendingIntent
import android.app.job.JobInfo import android.app.job.JobInfo
import android.app.job.JobScheduler import android.app.job.JobScheduler
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@ -15,6 +16,7 @@ import androidx.collection.isNotEmpty
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.StubApk
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.JobService import com.topjohnwu.magisk.core.JobService
@ -23,8 +25,18 @@ import com.topjohnwu.magisk.core.base.IActivityExtension
import com.topjohnwu.magisk.core.cmp import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.core.di.ServiceLocator import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.intent import com.topjohnwu.magisk.core.intent
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.forEach
import com.topjohnwu.magisk.core.ktx.set import com.topjohnwu.magisk.core.ktx.set
import com.topjohnwu.magisk.core.ktx.withStreams
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.core.utils.ProgressInputStream import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -32,7 +44,13 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.ResponseBody import okhttp3.ResponseBody
import timber.log.Timber import timber.log.Timber
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
/** /**
* This class drives the execution of file downloads and notification management. * This class drives the execution of file downloads and notification management.
@ -51,7 +69,16 @@ import java.io.InputStream
* For API 23 - 33, we use a foreground service as a session. * For API 23 - 33, we use a foreground service as a session.
* For API 34 and higher, we use user-initiated job services as a session. * For API 34 and higher, we use user-initiated job services as a session.
*/ */
class DownloadEngine(session: DownloadSession) : DownloadSession by session, DownloadNotifier { class DownloadEngine(
private val session: Session
) {
interface Session {
val context: Context
fun attachNotification(id: Int, builder: Notification.Builder)
fun onDownloadComplete()
}
companion object { companion object {
const val ACTION = "com.topjohnwu.magisk.DOWNLOAD" const val ACTION = "com.topjohnwu.magisk.DOWNLOAD"
@ -72,35 +99,33 @@ class DownloadEngine(session: DownloadSession) : DownloadSession by session, Dow
} }
} }
private fun createBroadcastIntent(context: Context, subject: Subject) = private fun createIntent(context: Context, subject: Subject) =
context.intent<com.topjohnwu.magisk.core.Receiver>() if (Build.VERSION.SDK_INT >= 34) {
.setAction(ACTION) context.intent<com.topjohnwu.magisk.core.Receiver>()
.putExtra(SUBJECT_KEY, subject) .setAction(ACTION)
.putExtra(SUBJECT_KEY, subject)
private fun createServiceIntent(context: Context, subject: Subject) = } else {
context.intent<com.topjohnwu.magisk.core.Service>() context.intent<com.topjohnwu.magisk.core.Service>()
.setAction(ACTION) .setAction(ACTION)
.putExtra(SUBJECT_KEY, subject) .putExtra(SUBJECT_KEY, subject)
}
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
fun getPendingIntent(context: Context, subject: Subject): PendingIntent { fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
val flag = PendingIntent.FLAG_IMMUTABLE or val flag = PendingIntent.FLAG_IMMUTABLE or
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_UPDATE_CURRENT or
PendingIntent.FLAG_ONE_SHOT PendingIntent.FLAG_ONE_SHOT
val intent = createIntent(context, subject)
return if (Build.VERSION.SDK_INT >= 34) { return if (Build.VERSION.SDK_INT >= 34) {
// On API 34+, download tasks are handled with a user-initiated job. // On API 34+, download tasks are handled with a user-initiated job.
// However, there is no way to schedule a new job directly with a pending intent. // However, there is no way to schedule a new job directly with a pending intent.
// As a workaround, we send the subject to a broadcast receiver and have it // As a workaround, we send the subject to a broadcast receiver and have it
// schedule the job for us. // schedule the job for us.
val intent = createBroadcastIntent(context, subject)
PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flag) PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flag)
} else if (Build.VERSION.SDK_INT >= 26) {
PendingIntent.getForegroundService(context, REQUEST_CODE, intent, flag)
} else { } else {
val intent = createServiceIntent(context, subject) PendingIntent.getService(context, REQUEST_CODE, intent, flag)
if (Build.VERSION.SDK_INT >= 26) {
PendingIntent.getForegroundService(context, REQUEST_CODE, intent, flag)
} else {
PendingIntent.getService(context, REQUEST_CODE, intent, flag)
}
} }
} }
@ -115,7 +140,6 @@ class DownloadEngine(session: DownloadSession) : DownloadSession by session, Dow
} }
} }
@SuppressLint("MissingPermission")
fun start(context: Context, subject: Subject) { fun start(context: Context, subject: Subject) {
if (Build.VERSION.SDK_INT >= 34) { if (Build.VERSION.SDK_INT >= 34) {
val scheduler = context.getSystemService<JobScheduler>()!! val scheduler = context.getSystemService<JobScheduler>()!!
@ -128,29 +152,24 @@ class DownloadEngine(session: DownloadSession) : DownloadSession by session, Dow
.setTransientExtras(extras) .setTransientExtras(extras)
.build() .build()
scheduler.schedule(info) scheduler.schedule(info)
} else if (Build.VERSION.SDK_INT >= 26) {
context.startForegroundService(createIntent(context, subject))
} else { } else {
val intent = createServiceIntent(context, subject) context.startService(createIntent(context, subject))
if (Build.VERSION.SDK_INT >= 26) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
} }
} }
} }
private val notifications = SparseArrayCompat<Notification.Builder>()
private var attachedId = -1
private val job = Job()
private val processor = DownloadProcessor(this)
private val network get() = ServiceLocator.networkService
fun download(subject: Subject) { fun download(subject: Subject) {
notifyUpdate(subject.notifyId) notifyUpdate(subject.notifyId)
CoroutineScope(job + Dispatchers.IO).launch { CoroutineScope(job + Dispatchers.IO).launch {
try { try {
val stream = network.fetchFile(subject.url).toProgressStream(subject) val stream = network.fetchFile(subject.url).toProgressStream(subject)
processor.handle(stream, subject) when (subject) {
is Subject.App -> handleApp(stream, subject)
is Subject.Module -> handleModule(stream, subject.file)
else -> stream.copyAndClose(subject.file.outputStream())
}
val activity = AppContext.foregroundActivity val activity = AppContext.foregroundActivity
if (activity != null && subject.autoLaunch) { if (activity != null && subject.autoLaunch) {
notifyRemove(subject.notifyId) notifyRemove(subject.notifyId)
@ -168,13 +187,16 @@ class DownloadEngine(session: DownloadSession) : DownloadSession by session, Dow
@Synchronized @Synchronized
fun reattach() { fun reattach() {
val builder = notifications[attachedId] ?: return val builder = notifications[attachedId] ?: return
attachNotification(attachedId, builder) session.attachNotification(attachedId, builder)
} }
private fun attach(id: Int, notification: Notification.Builder) { private val notifications = SparseArrayCompat<Notification.Builder>()
attachedId = id private var attachedId = -1
attachNotification(id, notification)
} private val job = Job()
private val context get() = session.context
private val network get() = ServiceLocator.networkService
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int { private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
val notification = notifyRemove(id)?.also(editor) ?: return -1 val notification = notifyRemove(id)?.also(editor) ?: return -1
@ -201,14 +223,19 @@ class DownloadEngine(session: DownloadSession) : DownloadSession by session, Dow
subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) } subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) }
} }
private fun attachNotification(id: Int, notification: Notification.Builder) {
attachedId = id
session.attachNotification(id, notification)
}
@Synchronized @Synchronized
override fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit) { private fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
val notification = (notifications[id] ?: Notifications.startProgress("").also { val notification = (notifications[id] ?: Notifications.startProgress("").also {
notifications[id] = it notifications[id] = it
}).apply(editor) }).apply(editor)
if (attachedId < 0) if (attachedId < 0)
attach(id, notification) attachNotification(id, notification)
else else
Notifications.mgr.notify(id, notification.build()) Notifications.mgr.notify(id, notification.build())
} }
@ -228,11 +255,11 @@ class DownloadEngine(session: DownloadSession) : DownloadSession by session, Dow
// There are still remaining notifications, pick one and attach to the session // There are still remaining notifications, pick one and attach to the session
val anotherId = notifications.keyAt(0) val anotherId = notifications.keyAt(0)
val notification = notifications.valueAt(0) val notification = notifications.valueAt(0)
attach(anotherId, notification) attachNotification(anotherId, notification)
} else { } else {
// No more notifications left, terminate the session // No more notifications left, terminate the session
attachedId = -1 attachedId = -1
onDownloadComplete() session.onDownloadComplete()
} }
} }
} }
@ -241,6 +268,90 @@ class DownloadEngine(session: DownloadSession) : DownloadSession by session, Dow
return n return n
} }
private 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 zf = ZipFile(updateApk)
val apk = context.cachedFile("stub.apk")
apk.delete()
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
zf.close()
// 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()
}
}
private suspend fun handleModule(src: InputStream, file: Uri) {
val input = ZipInputStream(src)
val output = ZipOutputStream(file.outputStream())
withStreams(input, output) { zin, zout ->
zout.putNextEntry(ZipEntry("META-INF/"))
zout.putNextEntry(ZipEntry("META-INF/com/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
context.assets.open("module_installer.sh").use { it.copyAll(zout) }
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray())
zin.forEach { entry ->
val path = entry.name
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) {
zin.copyAll(zout)
}
}
}
}
}
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()
}
}
private fun ResponseBody.toProgressStream(subject: Subject): InputStream { private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
val max = contentLength() val max = contentLength()
val total = max.toFloat() / 1048576 val total = max.toFloat() / 1048576

View File

@ -1,122 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -8,7 +8,7 @@ import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.core.net.toUri import androidx.core.net.toUri
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.UpdateInfo import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.core.model.module.OnlineModule import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
@ -38,7 +38,7 @@ abstract class Subject : Parcelable {
@Parcelize @Parcelize
class App( class App(
private val json: UpdateInfo = Info.update, private val json: MagiskJson = Info.remote.magisk,
override val notifyId: Int = Notifications.nextId() override val notifyId: Int = Notifications.nextId()
) : Subject() { ) : Subject() {
override val title: String get() = "Magisk-${json.version}(${json.versionCode})" override val title: String get() = "Magisk-${json.version}(${json.versionCode})"

View File

@ -2,11 +2,7 @@ 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.BroadcastReceiver import android.content.*
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
@ -27,6 +23,7 @@ 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,7 +8,6 @@ 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
@ -18,14 +17,24 @@ 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 <In : Closeable, Out : Closeable> withInOut( inline fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
input: In, var entry: ZipEntry? = nextEntry
output: Out, while (entry != null) {
callback(entry)
entry = nextEntry
}
}
inline fun <In : InputStream, Out : OutputStream> withStreams(
inStream: In,
outStream: Out,
withBoth: (In, Out) -> Unit withBoth: (In, Out) -> Unit
) { ) {
input.use { reader -> inStream.use { reader ->
output.use { writer -> outStream.use { writer ->
withBoth(reader, writer) withBoth(reader, writer)
} }
} }
@ -55,7 +64,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
) = withInOut(this, out) { i, o -> i.copyAll(o, bufferSize, dispatcher) } ) = withStreams(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

@ -1,23 +1,17 @@
package com.topjohnwu.magisk.core.model package com.topjohnwu.magisk.core.model
import android.os.Parcelable import android.os.Parcelable
import com.squareup.moshi.FromJson
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.ToJson
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
class UpdateJson( data class UpdateInfo(
val magisk: UpdateInfo = UpdateInfo(), val magisk: MagiskJson = MagiskJson(),
) )
@Parcelize @Parcelize
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class UpdateInfo( data class MagiskJson(
val version: String = "", val version: String = "",
val versionCode: Int = -1, val versionCode: Int = -1,
val link: String = "", val link: String = "",
@ -33,34 +27,11 @@ data class ModuleJson(
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ReleaseAssets( data class CommitInfo(
val name: String, val sha: String
@Json(name = "browser_download_url") val url: String,
) )
@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class DateTime
class DateTimeAdapter {
@ToJson
fun toJson(@DateTime date: LocalDateTime): String {
return date.toString()
}
@FromJson
@DateTime
fun fromJson(date: String): LocalDateTime {
return LocalDateTime.parse(date, ISO_OFFSET_DATE_TIME)
}
}
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Release( data class BranchInfo(
@Json(name = "tag_name") val tag: String, val commit: CommitInfo
val name: String,
val prerelease: Boolean,
val assets: List<ReleaseAssets>,
val body: String,
@Json(name = "created_at") @DateTime val createdTime: LocalDateTime,
) )

View File

@ -5,15 +5,14 @@ import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.di.ServiceLocator import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.utils.RootUtils import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.nio.ExtendedFile
import kotlinx.coroutines.Dispatchers 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.Locale import java.util.*
data class LocalModule( data class LocalModule(
private val base: ExtendedFile, private val path: String,
) : Module() { ) : Module() {
private val svc get() = ServiceLocator.networkService private val svc get() = ServiceLocator.networkService
@ -25,18 +24,20 @@ data class LocalModule(
var description: String = "" var description: String = ""
var updateInfo: OnlineModule? = null var updateInfo: OnlineModule? = null
var outdated = false var outdated = false
private var updateUrl: String = "" private var updateUrl: String = ""
private val removeFile = RootUtils.fs.getFile(path, "remove")
private val disableFile = RootUtils.fs.getFile(path, "disable")
private val updateFile = RootUtils.fs.getFile(path, "update")
private val riruFolder = RootUtils.fs.getFile(path, "riru")
private val zygiskFolder = RootUtils.fs.getFile(path, "zygisk")
private val unloaded = RootUtils.fs.getFile(zygiskFolder, "unloaded")
private val removeFile = base.getChildFile("remove") val updated: Boolean get() = updateFile.exists()
private val disableFile = base.getChildFile("disable") val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
private val updateFile = base.getChildFile("update") val isZygisk: Boolean get() = zygiskFolder.exists()
val zygiskFolder = base.getChildFile("zygisk") val zygiskUnloaded: Boolean get() = unloaded.exists()
val hasAction: Boolean;
val updated get() = updateFile.exists()
val isRiru = (id == "riru-core") || base.getChildFile("riru").exists()
val isZygisk = zygiskFolder.exists()
val zygiskUnloaded = zygiskFolder.getChildFile("unloaded").exists()
val hasAction = base.getChildFile("action.sh").exists()
var enable: Boolean var enable: Boolean
get() = !disableFile.exists() get() = !disableFile.exists()
@ -89,16 +90,19 @@ data class LocalModule(
init { init {
runCatching { runCatching {
parseProps(Shell.cmd("dos2unix < $base/module.prop").exec().out) parseProps(Shell.cmd("dos2unix < $path/module.prop").exec().out)
} }
if (id.isEmpty()) { if (id.isEmpty()) {
id = base.name val sep = path.lastIndexOf('/')
id = path.substring(sep + 1)
} }
if (name.isEmpty()) { if (name.isEmpty()) {
name = id name = id
} }
hasAction = RootUtils.fs.getFile(path, "action.sh").exists()
} }
suspend fun fetch(): Boolean { suspend fun fetch(): Boolean {
@ -121,14 +125,14 @@ data class LocalModule(
companion object { companion object {
fun loaded() = RootUtils.fs.getFile(Const.MODULE_PATH).exists() fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
suspend fun installed() = withContext(Dispatchers.IO) { suspend fun installed() = withContext(Dispatchers.IO) {
RootUtils.fs.getFile(Const.MODULE_PATH) RootUtils.fs.getFile(Const.MAGISK_PATH)
.listFiles() .listFiles()
.orEmpty() .orEmpty()
.filter { !it.isFile && !it.isHidden } .filter { !it.isFile && !it.isHidden }
.map { LocalModule(it) } .map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
.sortedBy { it.name.lowercase(Locale.ROOT) } .sortedBy { it.name.lowercase(Locale.ROOT) }
} }
} }

View File

@ -1,32 +1,22 @@
package com.topjohnwu.magisk.core.model.su package com.topjohnwu.magisk.core.model.su
import com.topjohnwu.magisk.core.data.magiskdb.MagiskDB class SuPolicy(val uid: Int) {
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
} }
fun toMap(): MutableMap<String, Any> { var policy: Int = INTERACTIVE
val until = if (remain <= 0) { var until: Long = -1L
remain var logging: Boolean = true
} else { var notification: Boolean = true
MagiskDB.Literal("(strftime(\"%s\", \"now\") + $remain)")
} fun toMap(): MutableMap<String, Any> = mutableMapOf(
return mutableMapOf( "uid" to uid,
"uid" to uid, "policy" to policy,
"policy" to policy, "until" to until,
"until" to until, "logging" to logging,
"logging" to logging, "notification" to notification
"notification" to notification )
)
}
} }

View File

@ -8,18 +8,15 @@ import com.topjohnwu.magisk.core.Config.Value.DEBUG_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.data.GithubApiServices import com.topjohnwu.magisk.core.data.GithubPageServices
import com.topjohnwu.magisk.core.data.RawUrl import com.topjohnwu.magisk.core.data.RawServices
import com.topjohnwu.magisk.core.model.Release
import com.topjohnwu.magisk.core.model.UpdateInfo
import retrofit2.HttpException import retrofit2.HttpException
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.time.format.DateTimeFormatter
class NetworkService( class NetworkService(
private val raw: RawUrl, private val pages: GithubPageServices,
private val api: GithubApiServices, private val raw: RawServices
) { ) {
suspend fun fetchUpdate() = safe { suspend fun fetchUpdate() = safe {
var info = when (Config.updateChannel) { var info = when (Config.updateChannel) {
@ -30,7 +27,7 @@ class NetworkService(
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl) CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
if (info.versionCode < Info.env.versionCode && if (info.magisk.versionCode < Info.env.versionCode &&
Config.updateChannel == DEFAULT_CHANNEL) { Config.updateChannel == DEFAULT_CHANNEL) {
Config.updateChannel = BETA_CHANNEL Config.updateChannel = BETA_CHANNEL
info = fetchBetaUpdate() info = fetchBetaUpdate()
@ -38,57 +35,12 @@ class NetworkService(
info info
} }
// Keep going through all release pages until we find a match // UpdateInfo
private suspend inline fun findRelease(predicate: (Release) -> Boolean): Release? { private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
var page = 1 private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
while (true) { private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
val response = api.fetchReleases(page = page) private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
val releases = response.body() ?: throw HttpException(response) private suspend fun fetchCustomUpdate(url: String) = pages.fetchUpdateJSON(url)
// Make sure it's sorted correctly
releases.sortByDescending { it.createdTime }
releases.find(predicate)?.let { return it }
if (response.headers()["link"]?.contains("rel=\"next\"", ignoreCase = true) == true) {
page += 1
} else {
return null
}
}
}
private fun Release.asPublicInfo(): UpdateInfo {
val version = tag.drop(1)
val date = createdTime.format(DateTimeFormatter.ofPattern("yyyy.M.d"))
return UpdateInfo(
version = version,
versionCode = (version.toFloat() * 1000).toInt(),
link = assets[0].url,
note = "## $date $name\n\n$body"
)
}
private fun Release.asCanaryInfo(assetSelector: String): UpdateInfo {
return UpdateInfo(
version = name.substring(8, 16),
versionCode = tag.drop(7).toInt(),
link = assets.find { it.name == assetSelector }!!.url,
note = "## $name\n\n$body"
)
}
private suspend fun fetchStableUpdate() = api.fetchLatestRelease().asPublicInfo()
private suspend fun fetchBetaUpdate() = findRelease { it.tag[0] == 'v' }!!.asPublicInfo()
private suspend fun fetchCanary() = findRelease { it.tag.startsWith("canary-") }!!
private suspend fun fetchCanaryUpdate() = fetchCanary().asCanaryInfo("app-release.apk")
private suspend fun fetchDebugUpdate() = fetchCanary().asCanaryInfo("app-debug.apk")
private suspend fun fetchCustomUpdate(url: String): UpdateInfo {
val info = raw.fetchUpdateJson(url).magisk
return info.let { it.copy(note = raw.fetchString(it.note)) }
}
private inline fun <T> safe(factory: () -> T): T? { private inline fun <T> safe(factory: () -> T): T? {
return try { return try {

View File

@ -4,7 +4,6 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.ByteBuffer;
public class ByteArrayStream extends ByteArrayOutputStream { public class ByteArrayStream extends ByteArrayOutputStream {
@ -28,8 +27,4 @@ public class ByteArrayStream extends ByteArrayOutputStream {
public ByteArrayInputStream getInputStream() { public ByteArrayInputStream getInputStream() {
return new ByteArrayInputStream(buf, 0, count); return new ByteArrayInputStream(buf, 0, count);
} }
public ByteBuffer toByteBuffer() {
return ByteBuffer.wrap(buf, 0, count);
}
} }

View File

@ -510,7 +510,7 @@ public class SignApk {
privateKey[0] = key; privateKey[0] = key;
// Generate, in memory, an APK signed using standard JAR Signature Scheme. // Generate, in memory, an APK signed using standard JAR Signature Scheme.
ByteArrayStream v1SignedApkBuf = new ByteArrayStream(); ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf); JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
// Use maximum compression for compressed entries because the APK lives forever on // Use maximum compression for compressed entries because the APK lives forever on
// the system partition. // the system partition.
@ -519,7 +519,8 @@ public class SignApk {
copyFiles(manifest, inputJar, outputJar, timestamp, alignment); copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
signFile(manifest, publicKey, privateKey, timestamp, outputJar); signFile(manifest, publicKey, privateKey, timestamp, outputJar);
outputJar.close(); outputJar.close();
ByteBuffer v1SignedApk = v1SignedApkBuf.toByteBuffer(); ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
v1SignedApkBuf.reset();
ByteBuffer[] outputChunks; ByteBuffer[] outputChunks;
List<ApkSignerV2.SignerConfig> signerConfigs = createV2SignerConfigs(privateKey, publicKey, List<ApkSignerV2.SignerConfig> signerConfigs = createV2SignerConfigs(privateKey, publicKey,

View File

@ -62,7 +62,7 @@ class SuRequestHandler(
return false return false
} }
output = File(fifo) output = File(fifo)
policy = policyDB.fetch(uid) ?: SuPolicy(uid) policy = 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,13 +81,15 @@ class SuRequestHandler(
return true return true
} }
suspend fun respond(action: Int, time: Long) { suspend fun respond(action: Int, time: Int) {
val until = if (time > 0)
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) +
TimeUnit.MINUTES.toSeconds(time.toLong())
else
time.toLong()
policy.policy = action policy.policy = action
if (time >= 0) { policy.until = until
policy.remain = TimeUnit.MINUTES.toSeconds(time)
} else {
policy.remain = time
}
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
@ -98,7 +100,7 @@ class SuRequestHandler(
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
} }
if (time >= 0) { if (until >= 0) {
policyDB.update(policy) policyDB.update(policy)
} }
} }

View File

@ -0,0 +1,80 @@
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,7 +4,6 @@ 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
@ -14,6 +13,7 @@ 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,9 +23,11 @@ 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
@ -36,7 +38,6 @@ 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
@ -132,15 +133,21 @@ 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 {
when { if (!xml.patchStrings {
it.contains(APP_PACKAGE_NAME) -> it.replace(APP_PACKAGE_NAME, pkg) for (i in it.indices) {
it.contains(PLACEHOLDER) -> generator.next() val s = it[i]
it == origLabel -> label.toString() if (s.contains(APP_PACKAGE_NAME)) {
else -> it it[i] = s.replace(APP_PACKAGE_NAME, pkg)
} 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) }
@ -154,87 +161,52 @@ object AppMigration {
} }
} }
private fun patchTest(apk: File, out: File, pkg: String): Boolean { private fun launchApp(activity: Activity, pkg: String) {
try { val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
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)
} }
context.startActivity(intent, options.toBundle()) activity.startActivity(intent, options.toBundle())
if (context is Activity) { activity.finish()
context.finish()
}
} }
suspend fun patchAndHide(context: Context, label: String, pkg: String? = null): Boolean { private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
val stub = File(context.cacheDir, "stub.apk") val stub = File(activity.cacheDir, "stub.apk")
try { try {
context.assets.open("stub.apk").writeTo(stub) activity.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 signature and package name if needed // Generate a new random package name and signature
val pkg = pkg ?: genPackageName() val repack = File(activity.cacheDir, "patched.apk")
val pkg = genPackageName()
Config.keyStoreRaw = "" Config.keyStoreRaw = ""
// Check and patch the test APK if (!patch(activity, stub, FileOutputStream(repack), pkg, label))
try { return false
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 cmd = "adb_pm_install $repack $pkg" val session = APKInstall.startSession(activity, pkg, onFailure) {
if (Shell.cmd(cmd).exec().isSuccess) {
Config.suManager = pkg Config.suManager = pkg
Shell.cmd("touch $AppApkPath").exec() Shell.cmd("touch $AppApkPath").exec()
launchApp(context, pkg) launchApp(activity, 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")
@ -245,25 +217,14 @@ object AppMigration {
setCancelable(false) setCancelable(false)
show() show()
} }
val success = withContext(Dispatchers.IO) { val onFailure = Runnable {
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
} }
return false if (!success) onFailure.run()
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -274,10 +235,30 @@ object AppMigration {
setCancelable(false) setCancelable(false)
show() show()
} }
if (!restoreApp(activity)) { val onFailure = Runnable {
dialog.dismiss()
activity.toast(R.string.failure, Toast.LENGTH_LONG) activity.toast(R.string.failure, Toast.LENGTH_LONG)
} }
dialog.dismiss() val apk = StubApk.current(activity)
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,6 +7,7 @@ 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
@ -46,14 +47,20 @@ open class FlashZip(
} }
} }
try { val isValid = try {
val binary = File(installDir, "update-binary") zipFile.unzip(installDir, "META-INF/com/google/android", true)
AppContext.assets.open("module_installer.sh").use { it.writeTo(binary) } val script = File(installDir, "updater-script")
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

@ -17,6 +17,7 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.di.ServiceLocator import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.copyAll import com.topjohnwu.magisk.core.ktx.copyAll
import com.topjohnwu.magisk.core.ktx.copyAndClose
import com.topjohnwu.magisk.core.ktx.writeTo import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.utils.DummyList import com.topjohnwu.magisk.core.utils.DummyList
import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
@ -46,6 +47,7 @@ import java.io.OutputStream
import java.io.PushbackInputStream import java.io.PushbackInputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.security.SecureRandom import java.security.SecureRandom
import java.util.Arrays
import java.util.Locale import java.util.Locale
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -132,6 +134,7 @@ abstract class MagiskInstallImpl protected constructor(
if (entry != null) { if (entry != null) {
val magisk32 = File(installDir, "magisk32") val magisk32 = File(installDir, "magisk32")
zf.getInputStream(entry).writeTo(magisk32) zf.getInputStream(entry).writeTo(magisk32)
magisk32.setExecutable(true)
} }
} }
} }
@ -146,15 +149,11 @@ abstract class MagiskInstallImpl protected constructor(
Os.symlink(lib.path, "$installDir/$name") Os.symlink(lib.path, "$installDir/$name")
} }
// Also extract magisk32 on 64-bit devices that supports 32-bit // Also symlink magisk32 on 64-bit devices that supports 32-bit
val abi32 = Const.CPU_ABI_32 val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir")
if (Process.is64Bit() && abi32 != null) { .get(info) as String?
val name = "lib/$abi32/libmagisk.so" if (lib32 != null) {
val entry = javaClass.classLoader!!.getResourceAsStream(name) Os.symlink("$lib32/libmagisk.so", "$installDir/magisk32");
if (entry != null) {
val magisk32 = File(installDir, "magisk32")
entry.writeTo(magisk32)
}
} }
} }
@ -192,8 +191,7 @@ abstract class MagiskInstallImpl protected constructor(
return true return true
} }
private suspend fun InputStream.copyAndCloseOut(out: OutputStream) = private suspend fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyAll(it) }
out.use { copyAll(it, 1024 * 1024) }
private class NoAvailableStream(s: InputStream) : FilterInputStream(s) { private class NoAvailableStream(s: InputStream) : FilterInputStream(s) {
// Make sure available is never called on the actual stream and always return 0 // Make sure available is never called on the actual stream and always return 0
@ -227,8 +225,8 @@ abstract class MagiskInstallImpl protected constructor(
console.add("- Processing tar file") console.add("- Processing tar file")
var entry: TarArchiveEntry? = tarIn.nextEntry var entry: TarArchiveEntry? = tarIn.nextEntry
fun decompressedStream(): InputStream { fun TarArchiveEntry.decompressedStream(): InputStream {
val stream = if (tarIn.currentEntry.name.endsWith(".lz4")) val stream = if (name.endsWith(".lz4"))
FramedLZ4CompressorInputStream(tarIn, true) else tarIn FramedLZ4CompressorInputStream(tarIn, true) else tarIn
return NoAvailableStream(stream) return NoAvailableStream(stream)
} }
@ -254,9 +252,9 @@ abstract class MagiskInstallImpl protected constructor(
if (bootItem != null) { if (bootItem != null) {
console.add("-- Extracting: ${bootItem.name}") console.add("-- Extracting: ${bootItem.name}")
decompressedStream().copyAndCloseOut(bootItem.file.newOutputStream()) entry.decompressedStream().copyAndCloseOut(bootItem.file.newOutputStream())
} else if (entry.name.contains("vbmeta.img")) { } else if (entry.name.contains("vbmeta.img")) {
val rawData = decompressedStream().readBytes() val rawData = entry.decompressedStream().readBytes()
// Valid vbmeta.img should be at least 256 bytes // Valid vbmeta.img should be at least 256 bytes
if (rawData.size < 256) if (rawData.size < 256)
continue continue
@ -289,7 +287,7 @@ abstract class MagiskInstallImpl protected constructor(
} else { } else {
console.add("-- Copying : ${entry.name}") console.add("-- Copying : ${entry.name}")
tarOut.putArchiveEntry(entry) tarOut.putArchiveEntry(entry)
tarIn.copyAll(tarOut) tarIn.copyAll(tarOut, bufferSize = 1024 * 1024)
tarOut.closeArchiveEntry() tarOut.closeArchiveEntry()
} }
entry = tarIn.nextEntry ?: break entry = tarIn.nextEntry ?: break
@ -431,7 +429,7 @@ abstract class MagiskInstallImpl protected constructor(
// Process input file // Process input file
try { try {
PushbackInputStream(uri.inputStream().buffered(1024 * 1024), 512).use { src -> PushbackInputStream(uri.inputStream(), 512).use { src ->
val head = ByteArray(512) val head = ByteArray(512)
if (src.read(head) != head.size) { if (src.read(head) != head.size) {
console.add("! Invalid input file") console.add("! Invalid input file")
@ -440,13 +438,12 @@ abstract class MagiskInstallImpl protected constructor(
src.unread(head) src.unread(head)
val magic = head.copyOf(4) val magic = head.copyOf(4)
val tarMagic = head.copyOfRange(257, 262) val tarMagic = Arrays.copyOfRange(head, 257, 262)
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) { srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
// tar file // tar file
outFile = MediaStoreUtils.getFile("$destName.tar") outFile = MediaStoreUtils.getFile("$destName.tar")
val os = outFile.uri.outputStream().buffered(1024 * 1024) outStream = TarArchiveOutputStream(outFile.uri.outputStream()).also {
outStream = TarArchiveOutputStream(os).also {
it.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR) it.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR)
it.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU) it.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU)
} }
@ -503,7 +500,7 @@ abstract class MagiskInstallImpl protected constructor(
bootItem.file = newBoot bootItem.file = newBoot
bootItem.copyTo(outStream as TarArchiveOutputStream) bootItem.copyTo(outStream as TarArchiveOutputStream)
} else { } else {
newBoot.newInputStream().use { it.copyAll(outStream, 1024 * 1024) } newBoot.newInputStream().copyAndClose(outStream)
} }
newBoot.delete() newBoot.delete()
@ -517,8 +514,6 @@ abstract class MagiskInstallImpl protected constructor(
outFile.delete() outFile.delete()
Timber.e(e) Timber.e(e)
return false return false
} finally {
outStream.close()
} }
// Fix up binaries // Fix up binaries
@ -602,9 +597,7 @@ abstract class MagiskInstallImpl protected constructor(
if (result) if (result)
return true return true
// Not every operation initializes installDir Shell.cmd("rm -rf $installDir").submit()
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(mapFn: (String) -> String): Boolean { fun patchStrings(patchFn: (Array<String>) -> Unit): Boolean {
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN) val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
fun findStringPool(): Int { fun findStringPool(): Int {
@ -65,9 +65,7 @@ class AXML(b: ByteArray) {
} }
val strArr = strList.toTypedArray() val strArr = strList.toTypedArray()
for (i in strArr.indices) { patchFn(strArr)
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,10 +2,6 @@ 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;
@ -33,15 +29,4 @@ 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,10 +11,13 @@ 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) { class NetworkObserver(context: Context): DefaultLifecycleObserver {
private val manager = context.getSystemService<ConnectivityManager>()!! private val manager = context.getSystemService<ConnectivityManager>()!!
private val networkCallback = object : ConnectivityManager.NetworkCallback() { private val networkCallback = object : ConnectivityManager.NetworkCallback() {
@ -52,17 +55,20 @@ class NetworkObserver(context: Context) {
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)
} }
fun postCurrentState() { override fun onStart(owner: LifecycleOwner) {
postValue( postCurrentState()
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) {
Info.resetUpdate() Info.remote = Info.EMPTY_REMOTE
Info.isConnected.postValue(b) Info.isConnected.postValue(b)
} }

View File

@ -0,0 +1,15 @@
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,50 @@
package com.topjohnwu.magisk.core.utils
import com.topjohnwu.magisk.core.ktx.copyAll
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream
import org.apache.commons.compress.archivers.zip.ZipFile
import java.io.File
import java.io.IOException
import java.io.InputStream
@Throws(IOException::class)
suspend fun File.unzip(folder: File, path: String = "", junkPath: Boolean = false) {
ZipFile.Builder().setFile(this).get().use { zip ->
for (entry in zip.entries) {
if (!entry.name.startsWith(path) || entry.isDirectory) {
// Ignore directories, only create files
continue
}
val name = if (junkPath)
entry.name.substring(entry.name.lastIndexOf('/') + 1)
else
entry.name
val dest = File(folder, name)
dest.parentFile?.mkdirs()
dest.outputStream().use { out -> zip.getInputStream(entry).copyAll(out) }
}
}
}
@Throws(IOException::class)
suspend fun InputStream.unzip(folder: File, path: String = "", junkPath: Boolean = false) {
ZipArchiveInputStream(this).use { zin ->
var entry: ZipArchiveEntry
while (true) {
entry = zin.nextEntry ?: break
if (!entry.name.startsWith(path) || entry.isDirectory) {
// Ignore directories, only create files
continue
}
val name = if (junkPath)
entry.name.substring(entry.name.lastIndexOf('/') + 1)
else
entry.name
val dest = File(folder, name)
dest.parentFile?.mkdirs()
dest.outputStream().use { out -> zin.copyAll(out) }
}
}
}

View File

@ -1,138 +0,0 @@
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 com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.test.Environment.Companion.EMPTY_ZYGISK
import com.topjohnwu.magisk.test.Environment.Companion.INVALID_ZYGISK
import com.topjohnwu.magisk.test.Environment.Companion.MOUNT_TEST
import com.topjohnwu.magisk.test.Environment.Companion.REMOVE_TEST
import com.topjohnwu.magisk.test.Environment.Companion.SEPOLICY_RULE
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.BeforeClass
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"
private lateinit var modules: List<LocalModule>
@BeforeClass
@JvmStatic
fun before() {
BaseTest.prerequisite()
runBlocking {
modules = LocalModule.installed()
}
}
}
@After
fun teardown() {
device.pressHome()
}
@Test
fun testModuleCount() {
var expected = 2
if (Environment.mount()) expected++
if (Environment.preinit()) expected++
if (Environment.lsposed()) expected++
if (Environment.shamiko()) expected++
assertEquals("Module count incorrect", expected, modules.size)
}
@Test
fun testLsposed() {
assumeTrue(Environment.lsposed())
val module = modules.find { it.id == "zygisk_lsposed" }
assertNotNull("zygisk_lsposed is not installed", module)
module!!
assertFalse("zygisk_lsposed is not enabled", module.zygiskUnloaded)
// Launch lsposed manager to ensure the module is active
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))
)
}
@Test
fun testModuleMount() {
assumeTrue(Environment.mount())
assertNotNull("$MOUNT_TEST is not installed", modules.find { it.id == MOUNT_TEST })
assertTrue(
"/system/etc/newfile should exist",
RootUtils.fs.getFile("/system/etc/newfile").exists()
)
assertFalse(
"/system/bin/screenrecord should not exist",
RootUtils.fs.getFile("/system/bin/screenrecord").exists()
)
val egg = RootUtils.fs.getFile("/system/app/EasterEgg").list() ?: arrayOf()
assertTrue(
"/system/app/EasterEgg should be empty",
egg.isEmpty()
)
}
@Test
fun testSepolicyRule() {
assumeTrue(Environment.preinit())
assertNotNull("$SEPOLICY_RULE is not installed", modules.find { it.id == SEPOLICY_RULE })
assertTrue(
"Module sepolicy.rule is not applied",
Shell.cmd("magiskpolicy --print-rules | grep -q magisk_test").exec().isSuccess
)
}
@Test
fun testEmptyZygiskModule() {
val module = modules.find { it.id == EMPTY_ZYGISK }
assertNotNull("$EMPTY_ZYGISK is not installed", module)
module!!
assertTrue("$EMPTY_ZYGISK should be zygisk unloaded", module.zygiskUnloaded)
}
@Test
fun testInvalidZygiskModule() {
val module = modules.find { it.id == INVALID_ZYGISK }
assertNotNull("$INVALID_ZYGISK is not installed", module)
module!!
assertTrue("$INVALID_ZYGISK should be zygisk unloaded", module.zygiskUnloaded)
}
@Test
fun testRemoveModule() {
assertNull("$REMOVE_TEST should be removed", modules.find { it.id == REMOVE_TEST })
}
}

View File

@ -1,27 +0,0 @@
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 appContext: Context get() = instrumentation.targetContext
val testContext: Context get() = instrumentation.context
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()
}
}
}

View File

@ -1,246 +0,0 @@
package com.topjohnwu.magisk.test
import android.app.Notification
import android.os.Build
import androidx.annotation.Keep
import androidx.core.net.toUri
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.download.DownloadNotifier
import com.topjohnwu.magisk.core.download.DownloadProcessor
import com.topjohnwu.magisk.core.ktx.cachedFile
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.tasks.FlashZip
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 com.topjohnwu.superuser.nio.ExtendedFile
import kotlinx.coroutines.runBlocking
import org.apache.commons.compress.archivers.zip.ZipFile
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import timber.log.Timber
import java.io.File
import java.io.PrintStream
@Keep
@RunWith(AndroidJUnit4::class)
class Environment : BaseTest {
companion object {
@BeforeClass
@JvmStatic
fun before() = BaseTest.prerequisite()
// The kernel running on emulators < API 26 does not play well with
// magic mount. Skip mount_test on those legacy platforms.
fun mount(): Boolean {
return Build.VERSION.SDK_INT >= 26
}
// It is possible that there are no suitable preinit partition to use
fun preinit(): Boolean {
return Shell.cmd("magisk --preinit-device").exec().isSuccess
}
fun lsposed(): Boolean {
return Build.VERSION.SDK_INT in 27..34
}
fun shamiko(): Boolean {
return Build.VERSION.SDK_INT >= 27
}
private const val MODULE_ERROR = "Module zip processing incorrect"
const val MOUNT_TEST = "mount_test"
const val SEPOLICY_RULE = "sepolicy_rule"
const val INVALID_ZYGISK = "invalid_zygisk"
const val REMOVE_TEST = "remove_test"
const val EMPTY_ZYGISK = "empty_zygisk"
}
object TimberLog : CallbackList<String>(Runnable::run) {
override fun onAddElement(e: String) {
Timber.i(e)
}
}
private fun checkModuleZip(file: File) {
// Make sure module processing is correct
ZipFile.Builder().setFile(file).get().use { zip ->
val meta = zip.entries
.asSequence()
.filter { it.name.startsWith("META-INF") }
.toMutableList()
assertEquals(MODULE_ERROR, 6, meta.size)
val binary = zip.getInputStream(
zip.getEntry("META-INF/com/google/android/update-binary")
).use { it.readBytes() }
val ref = appContext.assets.open("module_installer.sh").use { it.readBytes() }
assertArrayEquals(MODULE_ERROR, ref, binary)
val script = zip.getInputStream(
zip.getEntry("META-INF/com/google/android/updater-script")
).use { it.readBytes() }
assertArrayEquals(MODULE_ERROR, "#MAGISK\n".toByteArray(), script)
}
}
private fun setupMountTest(root: ExtendedFile) {
val error = "$MOUNT_TEST setup failed"
val path = root.getChildFile(MOUNT_TEST)
// Create /system/etc/newfile
val etc = path.getChildFile("system").getChildFile("etc")
assertTrue(error, etc.mkdirs())
assertTrue(error, etc.getChildFile("newfile").createNewFile())
// Create /system/app/EasterEgg/.replace
val egg = path.getChildFile("system").getChildFile("app").getChildFile("EasterEgg")
assertTrue(error, egg.mkdirs())
assertTrue(error, egg.getChildFile(".replace").createNewFile())
// Delete /system/bin/screenrecord
val bin = path.getChildFile("system").getChildFile("bin")
assertTrue(error, bin.mkdirs())
assertTrue(error, Shell.cmd("mknod $bin/screenrecord c 0 0").exec().isSuccess)
assertTrue(error, Shell.cmd("set_default_perm $path").exec().isSuccess)
}
private fun setupSepolicyRuleModule(root: ExtendedFile) {
val error = "$SEPOLICY_RULE setup failed"
val path = root.getChildFile(SEPOLICY_RULE)
assertTrue(error, path.mkdirs())
// Add sepolicy patch
PrintStream(path.getChildFile("sepolicy.rule").newOutputStream()).use {
it.println("type magisk_test domain")
}
assertTrue(error, Shell.cmd(
"set_default_perm $path",
"copy_preinit_files"
).exec().isSuccess)
}
private fun setupEmptyZygiskModule(root: ExtendedFile) {
val error = "$EMPTY_ZYGISK setup failed"
val path = root.getChildFile(EMPTY_ZYGISK)
// Create an empty zygisk folder
val module = LocalModule(path)
assertTrue(error, module.zygiskFolder.mkdirs())
}
private fun setupInvalidZygiskModule(root: ExtendedFile) {
val error = "$INVALID_ZYGISK setup failed"
val path = root.getChildFile(INVALID_ZYGISK)
// Create invalid zygisk libraries
val module = LocalModule(path)
assertTrue(error, module.zygiskFolder.mkdirs())
assertTrue(error, module.zygiskFolder.getChildFile("armeabi-v7a.so").createNewFile())
assertTrue(error, module.zygiskFolder.getChildFile("arm64-v8a.so").createNewFile())
assertTrue(error, module.zygiskFolder.getChildFile("x86.so").createNewFile())
assertTrue(error, module.zygiskFolder.getChildFile("x86_64.so").createNewFile())
assertTrue(error, Shell.cmd("set_default_perm $path").exec().isSuccess)
}
private fun setupRemoveModule(root: ExtendedFile) {
val error = "$REMOVE_TEST setup failed"
val path = root.getChildFile(REMOVE_TEST)
// Create a new module but mark is as "remove"
val module = LocalModule(path)
assertTrue(error, path.mkdirs())
assertTrue(error, path.getChildFile("service.sh").createNewFile())
module.remove = true
assertTrue(error, Shell.cmd("set_default_perm $path").exec().isSuccess)
}
@Test
fun setupEnvironment() {
runBlocking {
assertTrue(
"Magisk setup failed",
MagiskInstaller.Emulator(TimberLog, TimberLog).exec()
)
}
val notify = object : DownloadNotifier {
override val context = appContext
override fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit) {}
}
val processor = DownloadProcessor(notify)
val shamiko = appContext.cachedFile("shamiko.zip")
runBlocking {
testContext.assets.open("shamiko.zip").use {
processor.handleModule(it, shamiko.toUri())
}
checkModuleZip(shamiko)
if (shamiko()) {
assertTrue(
"Shamiko installation failed",
FlashZip(shamiko.toUri(), TimberLog, TimberLog).exec()
)
}
}
val lsp = appContext.cachedFile("lsposed.zip")
runBlocking {
testContext.assets.open("lsposed.zip").use {
processor.handleModule(it, lsp.toUri())
}
checkModuleZip(lsp)
if (lsposed()) {
assertTrue(
"LSPosed installation failed",
FlashZip(lsp.toUri(), TimberLog, TimberLog).exec()
)
}
}
val root = RootUtils.fs.getFile(Const.MODULE_PATH)
if (mount()) { setupMountTest(root) }
if (preinit()) { setupSepolicyRuleModule(root) }
setupEmptyZygiskModule(root)
setupInvalidZygiskModule(root)
setupRemoveModule(root)
}
@Test
fun setupAppHide() {
runBlocking {
assertTrue(
"App hiding failed",
AppMigration.patchAndHide(
context = appContext,
label = "Settings",
pkg = "repackaged.$APP_PACKAGE_NAME"
)
)
}
}
@Test
fun setupAppRestore() {
runBlocking {
assertTrue(
"App restoration failed",
AppMigration.restoreApp(appContext)
)
}
}
}

View File

@ -1,86 +0,0 @@
package com.topjohnwu.magisk.test
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.ParcelFileDescriptor.AutoCloseInputStream
import androidx.annotation.Keep
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.model.su.SuPolicy
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit
@Keep
@RunWith(AndroidJUnit4::class)
class MagiskAppTest : BaseTest {
companion object {
@BeforeClass
@JvmStatic
fun before() = BaseTest.prerequisite()
}
@Test
fun testZygisk() {
assertTrue("Zygisk should be enabled", Info.isZygiskEnabled)
}
@Test
fun testSuRequest() {
// Bypass the need to actually show a dialog
Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW
Config.prefs.edit().commit()
// Inject an undetermined + mute logging policy for ADB shell
val policy = SuPolicy(
uid = 2000,
logging = false,
notification = false,
remain = 0L
)
runBlocking {
ServiceLocator.policyDB.update(policy)
}
val filter = IntentFilter(Intent.ACTION_VIEW)
filter.addCategory(Intent.CATEGORY_DEFAULT)
val monitor = instrumentation.addMonitor(filter, null, false)
// Try to call su from ADB shell
val cmd = if (Build.VERSION.SDK_INT < 24) {
// API 23 runs executeShellCommand as root
"/system/xbin/su 2000 su -c id"
} else {
"su -c id"
}
val pfd = uiAutomation.executeShellCommand(cmd)
// Make sure SuRequestActivity is launched
val suRequest = monitor.waitForActivityWithTimeout(TimeUnit.SECONDS.toMillis(10))
assertNotNull("SuRequestActivity is not launched", suRequest)
// Check that the request went through
AutoCloseInputStream(pfd).reader().use {
assertTrue(
"Cannot grant root permission from shell",
it.readText().contains("uid=0")
)
}
// Check that the database is updated
runBlocking {
val policy = ServiceLocator.policyDB.fetch(2000)
?: throw AssertionError("PolicyDB is invalid")
assertEquals("Policy for shell is incorrect", SuPolicy.ALLOW, policy.policy)
}
}
}

View File

@ -6,25 +6,25 @@
<string name="logs">السجلات</string> <string name="logs">السجلات</string>
<string name="settings">الإعدادات</string> <string name="settings">الإعدادات</string>
<string name="install">تثبيت</string> <string name="install">تثبيت</string>
<string name="section_home">الصفحة الرئيسية</string> <string name="section_home">الأساسي</string>
<string name="section_theme">المظهر</string> <string name="section_theme">السِمات</string>
<!--Home--> <!--Home-->
<string name="no_connection">لا يوجد إتصال</string> <string name="no_connection">لا يوجد إتصال</string>
<string name="app_changelog">تفاصيل التحديث</string> <string name="app_changelog">تفاصيل التحديث</string>
<string name="loading">جارٍ التحميل...</string> <string name="loading">جارٍ التحميل...</string>
<string name="update">تحديث</string> <string name="update">تحديث</string>
<string name="not_available">غير متوفر</string> <string name="not_available">غير/متوفر</string>
<string name="hide">إخفاء</string> <string name="hide">إخفاء</string>
<string name="home_package">الحزمة</string> <string name="home_package">الحزمة</string>
<string name="home_support_title">تبرع لنا</string> <string name="home_support_title">تبرع لنا</string>
<string name="home_item_source">الكود المصدري للتطبيق</string> <string name="home_item_source">الكود المصدري للتطبيق</string>
<string name="home_support_content">ماجيسك هو، وسيظل دوماً، مجانياّ و مفتوح المصدر، اظهر اهتمامك لنا لكي نبقيه هكذا بدعم مالي صغير</string> <string name="home_support_content">مـاجـيسك هي، وستظل دوماً، مجانيةً و مفتوحة المصدر، اظهر اهتمامك لنا لكي نبقيها هكذا بدعم مالي صغير</string>
<string name="home_installed_version">تم التثبيت</string> <string name="home_installed_version">تم التثبيت</string>
<string name="home_latest_version">آخر إصدار</string> <string name="home_latest_version">آخر إصدار</string>
<string name="invalid_update_channel">مصدر التحديث غير صالح</string> <string name="invalid_update_channel">مصدر التحديث غير صالح</string>
<string name="uninstall_magisk_title">إلغاء تثبيت ماجيسك</string> <string name="uninstall_magisk_title">إلغاء تثبيت Magisk</string>
<string name="uninstall_magisk_msg">ستُعطل/ستُحذف جميع الإضافات. سيُحذف الروت، وربما ستشفر بياناتك إذا لم تكن غير مشفرة حالياً.</string> <string name="uninstall_magisk_msg">ستُعطل/ستُحذف جميع الإضافات. سيُحذف الروت، وربما ستشفر بياناتك إذا لم تكن غير مشفرة حالياً.</string>
<!--Install--> <!--Install-->
@ -34,7 +34,7 @@
<string name="install_options_title">الخيارات</string> <string name="install_options_title">الخيارات</string>
<string name="install_method_title">الطريقة</string> <string name="install_method_title">الطريقة</string>
<string name="install_next">التالي</string> <string name="install_next">التالي</string>
<string name="install_start">هيا بنا</string> <string name="install_start">هيا، بنا</string>
<string name="manager_download_install">اضغط للتنزيل و التثبيت</string> <string name="manager_download_install">اضغط للتنزيل و التثبيت</string>
<string name="direct_install">تثبيت مباشر (موصى بها)</string> <string name="direct_install">تثبيت مباشر (موصى بها)</string>
<string name="install_inactive_slot">التثبيت على المنطقة الغير نشطة (بعد OTA)</string> <string name="install_inactive_slot">التثبيت على المنطقة الغير نشطة (بعد OTA)</string>
@ -87,15 +87,15 @@
<!-- MagiskHide --> <!-- MagiskHide -->
<string name="show_system_app">إظهار برامج النظام</string> <string name="show_system_app">إظهار برامج النظام</string>
<string name="hide_filter_hint">البحث بالاسم</string> <string name="hide_filter_hint">البحث بالإسم</string>
<string name="hide_search">ابحث</string> <string name="hide_search">ابحث</string>
<!--Module Fragment--> <!--Module Fragment-->
<string name="no_info_provided">ا تتوفر معلومات)</string> <string name="no_info_provided">م توفر معلومات)</string>
<string name="reboot_recovery">إعادة التشغيل إلى Recovery</string> <string name="reboot_recovery">إعادة التشغيل إلى Recovery</string>
<string name="reboot_bootloader">إعادة التشغيل إلى Bootloader</string> <string name="reboot_bootloader">إعادة التشغيل إلى Bootloader</string>
<string name="reboot_download">إعادة التشغيل إلى وضـع Odin</string> <string name="reboot_download">إعادة التشغيل إلى وضـع Odin</string>
<string name="reboot_edl">إعادة التشغيل إلى EDL</string> <string name="reboot_edl">إعادة التشغيل إلى وضـعية EDL</string>
<string name="module_version_author">%1$sبواسطة%2$s</string> <string name="module_version_author">%1$sبواسطة%2$s</string>
<string name="module_state_remove">إزالة </string> <string name="module_state_remove">إزالة </string>
<string name="module_state_restore">إسترجاع</string> <string name="module_state_restore">إسترجاع</string>
@ -104,15 +104,15 @@
<string name="external_rw_permission_denied">امنحني إذن الولوج للذاكرة الداخلية</string> <string name="external_rw_permission_denied">امنحني إذن الولوج للذاكرة الداخلية</string>
<!--Settings --> <!--Settings -->
<string name="settings_dark_mode_title">المظهر</string> <string name="settings_dark_mode_title">وضـعية الِسمات</string>
<string name="settings_dark_mode_message">حدد المظهر الذي يناسب ذوقك</string> <string name="settings_dark_mode_message">حدد الوضـع الذي يناسب ذوقك</string>
<string name="settings_dark_mode_light">الوضـع المضيء</string> <string name="settings_dark_mode_light">الوضـع المضيء</string>
<string name="settings_dark_mode_system">اتبّع النظام</string> <string name="settings_dark_mode_system">اتبّع النظام</string>
<string name="settings_dark_mode_dark">الوضع المظلم</string> <string name="settings_dark_mode_dark">وضـع الظلام</string>
<string name="settings_download_path_title">مسار التحميل</string> <string name="settings_download_path_title">مسار التحميل</string>
<string name="settings_download_path_message">ستحمل الملفات إلى %1$s</string> <string name="settings_download_path_message">ستحمل الملفات إلى %1$s</string>
<string name="language">اللغة</string> <string name="language">اللغة</string>
<string name="system_default">(الإفتراضي)</string> <string name="system_default">(الأفتراضي)</string>
<string name="settings_check_update_title">تحقق من التحديثات</string> <string name="settings_check_update_title">تحقق من التحديثات</string>
<string name="settings_check_update_summary">التحقق من التحديثات في الخلفية بشكل دوري</string> <string name="settings_check_update_summary">التحقق من التحديثات في الخلفية بشكل دوري</string>
<string name="settings_update_channel_title">مصدر التحديثات</string> <string name="settings_update_channel_title">مصدر التحديثات</string>
@ -123,8 +123,8 @@
<string name="settings_hosts_title">موانع الاعلانات</string> <string name="settings_hosts_title">موانع الاعلانات</string>
<string name="settings_hosts_summary">حجب الاعلانات دون تعديل النظام</string> <string name="settings_hosts_summary">حجب الاعلانات دون تعديل النظام</string>
<string name="settings_hosts_toast">تم تمكين خاصية حجب الاعلانات</string> <string name="settings_hosts_toast">تم تمكين خاصية حجب الاعلانات</string>
<string name="settings_app_name_hint">الاسم الجديد</string> <string name="settings_app_name_hint">الإسم الجديد</string>
<string name="settings_app_name_helper">التطبيق الجديد سوف يملك هذا الاسم</string> <string name="settings_app_name_helper">التطبيق الجديد سوف يملك هذا الإسم</string>
<string name="settings_app_name_error">الصيغة غير مقبولة</string> <string name="settings_app_name_error">الصيغة غير مقبولة</string>
<string name="settings_su_app_adb">التطبيقات و ADB</string> <string name="settings_su_app_adb">التطبيقات و ADB</string>
<string name="settings_su_app">التطبيقات فقط</string> <string name="settings_su_app">التطبيقات فقط</string>
@ -140,51 +140,51 @@
<string name="auto_response">الفعل التلقائي</string> <string name="auto_response">الفعل التلقائي</string>
<string name="request_timeout">المهلة قبل الفعل التلقائي</string> <string name="request_timeout">المهلة قبل الفعل التلقائي</string>
<string name="superuser_notification">إشعارات طلبات الروت</string> <string name="superuser_notification">إشعارات طلبات الروت</string>
<string name="settings_su_reauth_title">إعادة المصادقة بعد التحديث</string> <string name="settings_su_reauth_title">إعادة المصادقة بعد الترقية</string>
<string name="settings_su_reauth_summary">أعد مصادقة صلاحيات الروت بعد تحديث التطبيق</string> <string name="settings_su_reauth_summary">أعد مصادقة صلاحيات الروت بعد إجراء ترقيات للتطبيق</string>
<string name="settings_customization">تخصيص</string> <string name="settings_customization">تخصيص</string>
<string name="multiuser_mode">نمط المستخدم المزدوج</string> <string name="multiuser_mode">نمط مستخدمين متعددين</string>
<string name="settings_owner_only">مالك الجهاز فقط</string> <string name="settings_owner_only">مالك الجهاز فقط</string>
<string name="settings_owner_manage">المالك هو من يحدد</string> <string name="settings_owner_manage">المالك هو من يحدد</string>
<string name="settings_user_independent">مستقل</string> <string name="settings_user_independent">مستقل </string>
<string name="owner_only_summary">للمالك فقط له صلاحيات الروت</string> <string name="owner_only_summary">للمالك فقط له صلاحيات الروت</string>
<string name="owner_manage_summary">فقط المالك من يرفض و يمنح صلاحيات الروت</string> <string name="owner_manage_summary">فقط المالك من يرفض و يمنح صلاحيات الروت</string>
<string name="user_independent_summary">كل مستخدم له قواعد روت خاصة به</string> <string name="user_independent_summary">كل مستخدم له قواعد روت خاصة به</string>
<string name="mount_namespace_mode">نمط Mount Namespace</string> <string name="mount_namespace_mode">نمط Mount Namespace</string>
<string name="settings_ns_global">نمط Namespace عام</string> <string name="settings_ns_global">نمط Namespace العام</string>
<string name="settings_ns_requester">نمط NameSpace متوارث</string> <string name="settings_ns_requester">نمط NameSpace المتوارث</string>
<string name="settings_ns_isolate">نمط NameSpace معزول</string> <string name="settings_ns_isolate">نمط NameSpace المعزول</string>
<string name="global_summary">جميع الجلسات الروت تستخدم NameSpace العام</string> <string name="global_summary">جميع الجلسات الروت تستخدم NameSpace العام</string>
<string name="requester_summary">جميع الجلسات الروت تستخدم NameSpace المتوارث</string> <string name="requester_summary">جميع الجلسات الروت تستخدم NameSpace المتوارث</string>
<string name="isolate_summary">جميع الجلسات الروت تستخدم NameSpace المعزول</string> <string name="isolate_summary">جميع الجلسات الروت تستخدم NameSpace المعزول</string>
<!--Notifications--> <!--Notifications-->
<string name="update_channel">تحديثات ماجيسك</string> <string name="update_channel">تحديثات Magisk</string>
<string name="progress_channel">إشعارات التقدم</string> <string name="progress_channel">إشعارات التقدم</string>
<string name="download_complete">اكتمل التنزيل</string> <string name="download_complete">اكتمل التنزيل</string>
<string name="download_file_error">فشل تنزيل الملف</string> <string name="download_file_error">فشل تنزيل الملف</string>
<string name="magisk_update_title">تحديث ماجيسك متوفر!</string> <string name="magisk_update_title">تحديث مـاجـيسك متوفر!</string>
<!--Toasts, Dialogs--> <!--Toasts, Dialogs-->
<string name="yes">نعم</string> <string name="yes">نعم</string>
<string name="no">لا</string> <string name="no">لا</string>
<string name="download">تنزيل</string> <string name="download">تنزيل</string>
<string name="reboot">إعادة التشغيل</string> <string name="reboot">إعادة التشغيل</string>
<string name="release_notes">معلومات الإصدار الجديد</string> <string name="release_notes">معلومات الأصدار الجديد</string>
<string name="flashing">يتم التثبيت...</string> <string name="flashing">يتم الحرق...</string>
<string name="done">تم!</string> <string name="done">تم!</string>
<string name="failure">فشل!</string> <string name="failure">فشل!</string>
<string name="open_link_failed_toast">لم يُعثر على تطبيق لفتح الرابط …</string> <string name="open_link_failed_toast">لم يُعثر على تطبيق لفتح الرابط …</string>
<string name="complete_uninstall">إلغاء التثبيت بالكامل</string> <string name="complete_uninstall">إلغاء التثبيت بالكامل</string>
<string name="restore_img">استعادة الصور</string> <string name="restore_img">استعادة الصور</string>
<string name="restore_img_msg">جار الإستعادة…</string> <string name="restore_img_msg">جار الأستعادة…</string>
<string name="restore_done">تم الإستعادة</string> <string name="restore_done">تم الأستعادة</string>
<string name="restore_fail">النسخة الإحتياطية الأصلية غير موجودة!</string> <string name="restore_fail">النسخة الاحتياطية الأصلية غير موجودة!</string>
<string name="setup_fail">فشل الإعداد</string> <string name="setup_fail">فشل التضبيط</string>
<string name="env_fix_title">الإعداد الإضافي مطلوب</string> <string name="env_fix_title"> الإعداد الأضافي مطلوب</string>
<string name="setup_msg">جار إعداد البيئة</string> <string name="setup_msg">جار تضبيت البيئة</string>
<string name="unsupport_magisk_title">إصدار ماجيسك غير مدعوم</string> <string name="unsupport_magisk_title">إصدار مـاجـيسك غير مدعوم</string>
</resources> </resources>

View File

@ -4,7 +4,7 @@
<string name="modules">Módulos</string> <string name="modules">Módulos</string>
<string name="superuser">Superusuariu</string> <string name="superuser">Superusuariu</string>
<string name="logs">Rexistru</string> <string name="logs">Rexistru</string>
<string name="settings">Configuración</string> <string name="settings">Axustes</string>
<string name="install">Instalar</string> <string name="install">Instalar</string>
<string name="section_home">Aniciu</string> <string name="section_home">Aniciu</string>
<string name="section_theme">Estilos</string> <string name="section_theme">Estilos</string>
@ -15,7 +15,7 @@
<string name="loading">Cargando…</string> <string name="loading">Cargando…</string>
<string name="update">Anovar</string> <string name="update">Anovar</string>
<string name="not_available">N/D</string> <string name="not_available">N/D</string>
<string name="hide">Esconder</string> <string name="hide">Anubrir</string>
<string name="home_package">Paquete</string> <string name="home_package">Paquete</string>
<string name="home_app_title">Aplicación</string> <string name="home_app_title">Aplicación</string>
<string name="home_notice_content">Baxa Magisk NAMÁS dende la páxina oficial de GitHub. ¡Los ficheros de fontes desconocíes puen ser maliciosos!</string> <string name="home_notice_content">Baxa Magisk NAMÁS dende la páxina oficial de GitHub. ¡Los ficheros de fontes desconocíes puen ser maliciosos!</string>
@ -39,10 +39,10 @@
<string name="manager_download_install">Primi equí pa baxalu ya instalalu</string> <string name="manager_download_install">Primi equí pa baxalu ya instalalu</string>
<string name="direct_install">Instalación direuta (aconséyase)</string> <string name="direct_install">Instalación direuta (aconséyase)</string>
<string name="install_inactive_slot">Instalar na ralura inactiva (darréu del OTA)</string> <string name="install_inactive_slot">Instalar na ralura inactiva (darréu del OTA)</string>
<string name="install_inactive_slot_msg">¡El preséu va arrincar OBLIGATORIAMENTE na ralura inactiva dempués de reaniciar!\nUsa esta opción namás dempués d\'acabar l\'anovamientu per OTA.\n¿Quies siguir?</string> <string name="install_inactive_slot_msg">¡El preséu va arrincar OBLIGATORIAMENTE na ralura inactiva darréu de reaniciar!\nUsa esta opción namás dempués d\'acabar l\'anovamientu per OTA.\n¿Quies siguir?</string>
<string name="setup_title">Configuración adicional</string> <string name="setup_title">Configuración adicional</string>
<string name="select_patch_file">Seleicionar y parchiar un ficheru</string> <string name="select_patch_file">Seleicionar y parchiar un ficheru</string>
<string name="patch_file_msg">Seleiciona una imaxe en bruto (*.img), un archivu d\'ODIN (*.tar) o un ficheru payload.bin (*.bin)</string> <string name="patch_file_msg">Seleiciona una imaxe en bruto (*.img) o un archivu d\'ODIN (*.tar)</string>
<string name="reboot_delay_toast">Reaniciando en 5 segundos…</string> <string name="reboot_delay_toast">Reaniciando en 5 segundos…</string>
<string name="flash_screen_title">Instalación</string> <string name="flash_screen_title">Instalación</string>
<!--Superuser--> <!--Superuser-->
@ -81,9 +81,6 @@
<string name="logs_cleared">El rexistru borróse correutamente</string> <string name="logs_cleared">El rexistru borróse correutamente</string>
<string name="pid">PID: %1$d</string> <string name="pid">PID: %1$d</string>
<string name="target_uid">UID de destín: %1$d</string> <string name="target_uid">UID de destín: %1$d</string>
<string name="target_pid">Mount ns target PID: %s</string>
<string name="selinux_context">Contestu de SELinux: %s</string>
<string name="supp_group">Grupu suplementariu: %s</string>
<!--SafetyNet--> <!--SafetyNet-->
<!--MagiskHide--> <!--MagiskHide-->
<string name="show_system_app">Aplicaciones del sistema</string> <string name="show_system_app">Aplicaciones del sistema</string>
@ -97,19 +94,15 @@
<string name="reboot_bootloader">Reaniciar al cargador d\'arrinque</string> <string name="reboot_bootloader">Reaniciar al cargador d\'arrinque</string>
<string name="reboot_download">Reaniciar al mou de descarga</string> <string name="reboot_download">Reaniciar al mou de descarga</string>
<string name="reboot_edl">Reaniciar al mou EDL</string> <string name="reboot_edl">Reaniciar al mou EDL</string>
<string name="reboot_safe_mode">Mou seguru</string>
<string name="module_version_author">%1$s por %2$s</string> <string name="module_version_author">%1$s por %2$s</string>
<string name="module_state_remove">Quitar</string> <string name="module_state_remove">Quitar</string>
<string name="module_action">Aición</string>
<string name="module_state_restore">Restaurar</string> <string name="module_state_restore">Restaurar</string>
<string name="module_action_install_external">Instalar dende l\'almacenamientu</string> <string name="module_action_install_external">Instalar dende l\'almacenamientu</string>
<string name="update_available">Hai un anovamientu disponible</string> <string name="update_available">Hai un anovamientu disponible</string>
<string name="suspend_text_riru">Suspendióse\'l módulu porque s\'activó «%1$s»</string> <string name="suspend_text_riru">Suspendióse\'l módulu porque s\'activó «%1$s»</string>
<string name="suspend_text_zygisk">Suspendióse\'l módulu porque nun s\'activó «%1$s»</string> <string name="suspend_text_zygisk">Suspendióse\'l módulu porque nun s\'activó «%1$s»</string>
<string name="zygisk_module_unloaded">El módulu de Zygisk nun cargó por haber incompatibilidaes</string> <string name="zygisk_module_unloaded">El módulu de Zygisk nun cargó pola mor d\'haber incompatibilidaes</string>
<string name="module_empty">Nun hai nengún módulu instaláu</string> <string name="module_empty">Nun hai nengún módulu instaláu</string>
<string name="confirm_install">¿Quies instalar el módulu «%1$s»?</string>
<string name="confirm_install_title">Confirmación de la instalación</string>
<!--Settings--> <!--Settings-->
<string name="settings_dark_mode_title">Mou del estilu</string> <string name="settings_dark_mode_title">Mou del estilu</string>
<string name="settings_dark_mode_message">¡Seleiciona\'l mou que meyor s\'adaute al to estilu!</string> <string name="settings_dark_mode_message">¡Seleiciona\'l mou que meyor s\'adaute al to estilu!</string>
@ -118,7 +111,7 @@
<string name="settings_dark_mode_dark">Escuridá</string> <string name="settings_dark_mode_dark">Escuridá</string>
<string name="settings_download_path_title">Camín de les descargues</string> <string name="settings_download_path_title">Camín de les descargues</string>
<string name="settings_download_path_message">Los ficheros van guardase en «%1$s»</string> <string name="settings_download_path_message">Los ficheros van guardase en «%1$s»</string>
<string name="settings_hide_app_title">Esconder Magisk</string> <string name="settings_hide_app_title">Anubrir Magisk</string>
<string name="settings_hide_app_summary">Instala una aplicación intermedia con una ID y una etiqueta al debalu</string> <string name="settings_hide_app_summary">Instala una aplicación intermedia con una ID y una etiqueta al debalu</string>
<string name="settings_restore_app_title">Restaurar el mou visible</string> <string name="settings_restore_app_title">Restaurar el mou visible</string>
<string name="settings_restore_app_summary">Fai que l\'aplicación orixinal vuelva ser visible</string> <string name="settings_restore_app_summary">Fai que l\'aplicación orixinal vuelva ser visible</string>
@ -156,19 +149,14 @@
<string name="auto_response">Rempuesta automática</string> <string name="auto_response">Rempuesta automática</string>
<string name="request_timeout">Tiempu d\'espera de les solicitúes</string> <string name="request_timeout">Tiempu d\'espera de les solicitúes</string>
<string name="superuser_notification">Avisu de superusuariu</string> <string name="superuser_notification">Avisu de superusuariu</string>
<string name="settings_su_reauth_title">Volver autenticar dempués d\'anovar</string> <string name="settings_su_reauth_title">Volver autenticar darréu d\'anovar</string>
<string name="settings_su_reauth_summary">Vuelve pidir los permisos de superusuariu dempués d\'anovar les aplicaciones</string> <string name="settings_su_reauth_summary">Vuelve pidir los permisos de superusuariu dempués d\'anovar les aplicaciones</string>
<string name="settings_su_tapjack_title">Proteición escontra\'l tapjacking</string> <string name="settings_su_tapjack_title">Proteición escontra\'l tapjacking</string>
<string name="settings_su_tapjack_summary">El diálogu de concesión de permisos de superusuariu nun respuende a la entrada mentanto lu torgue otra ventana o superposición</string> <string name="settings_su_tapjack_summary">El diálogu de concesión de permisos de superusuariu nun respuende a la entrada mentanto lu torgue otra ventana o superposición</string>
<string name="settings_su_auth_title">Autenticación d\'usuariu</string>
<string name="settings_su_auth_summary">Pide l\'autenticación demientres les solicitúes de superusuariu</string>
<string name="settings_su_auth_insecure">Nun se configuró nengún métodu d\'autenticación nel preséu</string>
<string name="settings_customization">Personalización</string> <string name="settings_customization">Personalización</string>
<string name="setting_add_shortcut_summary">Amiesta un atayu a la pantalla d\'aniciu en casu de que\'l nome y l\'iconu seyan difíciles de reconocer dempués d\'esconder l\'aplicación</string> <string name="setting_add_shortcut_summary">Amiesta un atayu a la pantalla d\'aniciu en casu de que\'l nome y l\'iconu seyan difíciles de reconocer darréu d\'anubrir l\'aplicación</string>
<string name="settings_doh_title">DNS per HTTPS</string> <string name="settings_doh_title">DNS per HTTPS</string>
<string name="settings_doh_description">Una igua alternativa pal envelenamientu de DNS en dalgunos países</string> <string name="settings_doh_description">Una igua alternativa pal envelenamientu de DNS en dalgunos países</string>
<string name="settings_random_name_title">Nome de la salida aleatoriu</string>
<string name="settings_random_name_description">Fai que\'l nome de ficheru de la salida de les imáxenes parchiaes y los ficheros tar seya aleatoriu pa impidir la deteición</string>
<string name="multiuser_mode">Mou de multiusuariu</string> <string name="multiuser_mode">Mou de multiusuariu</string>
<string name="settings_owner_only">Namás el propietariu del preséu</string> <string name="settings_owner_only">Namás el propietariu del preséu</string>
<string name="settings_owner_manage">El propietariu xestionáu del preséu</string> <string name="settings_owner_manage">El propietariu xestionáu del preséu</string>
@ -198,14 +186,11 @@
<string name="repo_install_title">Instalación de: %1$s %2$s (%3$d)</string> <string name="repo_install_title">Instalación de: %1$s %2$s (%3$d)</string>
<string name="download">Baxar</string> <string name="download">Baxar</string>
<string name="reboot">Reaniciar</string> <string name="reboot">Reaniciar</string>
<string name="close">Zarrar</string>
<string name="release_notes">Notes de la versión</string> <string name="release_notes">Notes de la versión</string>
<string name="flashing">Flaxando…</string> <string name="flashing">Flaxando…</string>
<string name="running">Executando…</string>
<string name="done">¡Fecho!</string> <string name="done">¡Fecho!</string>
<string name="done_action">Completóse l\'aición de: %1$s</string>
<string name="failure">¡Falló!</string> <string name="failure">¡Falló!</string>
<string name="hide_app_title">Escondiendo l\'aplicación Magisk…</string> <string name="hide_app_title">Anubriendo l\'aplicación Magisk…</string>
<string name="open_link_failed_toast">Nun s\'atopó nenguna aplicación p\'abrir l\'enllaz</string> <string name="open_link_failed_toast">Nun s\'atopó nenguna aplicación p\'abrir l\'enllaz</string>
<string name="complete_uninstall">Desinstalar dafechu</string> <string name="complete_uninstall">Desinstalar dafechu</string>
<string name="restore_img">Restaurar les imáxenes</string> <string name="restore_img">Restaurar les imáxenes</string>
@ -215,7 +200,6 @@
<string name="setup_fail">La configuración falló</string> <string name="setup_fail">La configuración falló</string>
<string name="env_fix_title">Configuración adicional</string> <string name="env_fix_title">Configuración adicional</string>
<string name="env_fix_msg">El preséu precisa una configuración adicional pa que Magisk funcione afayadizamente. ¿Quies siguir y reaniciar?</string> <string name="env_fix_msg">El preséu precisa una configuración adicional pa que Magisk funcione afayadizamente. ¿Quies siguir y reaniciar?</string>
<string name="env_full_fix_msg">El preséu precisa volver flaxar Magisk pa que funcione afayadizamente. Volvi instalar Magisk dientro de l\'aplicación porque\'l mou de recuperación nun pue consiguir la información correuta del preséu.</string>
<string name="setup_msg">Executando la configuración del entornu…</string> <string name="setup_msg">Executando la configuración del entornu…</string>
<string name="unsupport_magisk_title">Versión non compatible</string> <string name="unsupport_magisk_title">Versión non compatible</string>
<string name="unsupport_magisk_msg">Esta versión de l\'aplicación nun ye compatible coles versiones de Magisk anteriores a la %1$s.\n\nL\'aplicación va comportase como si Magisk nun tuviere instaláu, anueva Magisk namás que puedas.</string> <string name="unsupport_magisk_msg">Esta versión de l\'aplicación nun ye compatible coles versiones de Magisk anteriores a la %1$s.\n\nL\'aplicación va comportase como si Magisk nun tuviere instaláu, anueva Magisk namás que puedas.</string>
@ -223,13 +207,12 @@
<string name="unsupport_system_app_msg">Esta aplicación nun se pue executar nel espaciu del sistema. Volvi instalala mas nel espaciu del usuariu.</string> <string name="unsupport_system_app_msg">Esta aplicación nun se pue executar nel espaciu del sistema. Volvi instalala mas nel espaciu del usuariu.</string>
<string name="unsupport_other_su_msg">Detectóse un binariu «su» que nun ye de Magisk. Quita cualesquier solución de root y/o volvi instalar Magisk.</string> <string name="unsupport_other_su_msg">Detectóse un binariu «su» que nun ye de Magisk. Quita cualesquier solución de root y/o volvi instalar Magisk.</string>
<string name="unsupport_external_storage_msg">Magisk ta instaláu nel almacenamientu esternu. Movi l\'aplicación al almacenamientu internu, por favor.</string> <string name="unsupport_external_storage_msg">Magisk ta instaláu nel almacenamientu esternu. Movi l\'aplicación al almacenamientu internu, por favor.</string>
<string name="unsupport_nonroot_stub_msg">Magisk nun pue siguir funcionando nel mou escondíu darréu que se perdió\'l root. Restaura\'l mou visible de l\'aplicación.</string> <string name="unsupport_nonroot_stub_msg">Magisk nun pue siguir funcionando nel mou anubríu darréu que se perdió\'l root. Restaura\'l mou visible de l\'aplicación.</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string> <string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">Concede\'l permisu d\'almacenamientu p\'activar esta funcionalidá</string> <string name="external_rw_permission_denied">Concede\'l permisu d\'almacenamientu p\'activar esta funcionalidá</string>
<string name="post_notifications_denied">Concede\'l permisu de los avisos p\'activar esta función</string>
<string name="install_unknown_denied">Permite la instalación d\'aplicaciones desconocíes p\'activar esta funcionalidá</string> <string name="install_unknown_denied">Permite la instalación d\'aplicaciones desconocíes p\'activar esta funcionalidá</string>
<string name="add_shortcut_title">Amestar un atayu a la pantalla d\'aniciu</string> <string name="add_shortcut_title">Amestar un atayu a la pantalla d\'aniciu</string>
<string name="add_shortcut_msg">Dempués d\'esconder esta aplicación, el so nome ya iconu van ser difíciles de reconocer. ¿Quies amestar un atayu a la pantalla d\'aniciu?</string> <string name="add_shortcut_msg">Darréu d\'anubrir esta aplicación, el so nome ya iconu van ser difíciles de reconocer. ¿Quies amestar un atayu a la pantalla d\'aniciu?</string>
<string name="app_not_found">Nun s\'atopó nenguna aplicación pa remanar esta aición</string> <string name="app_not_found">Nun s\'atopó nenguna aplicación pa remanar esta aición</string>
<string name="reboot_apply_change">Reanicia p\'aplicar los cambeos</string> <string name="reboot_apply_change">Reanicia p\'aplicar los cambeos</string>
<string name="restore_app_confirmation">Esta aición va restaurar l\'aplicación orixinal y desanicia la intermedia. ¿De xuru que quies facelo?</string> <string name="restore_app_confirmation">Esta aición va restaurar l\'aplicación orixinal y desanicia la intermedia. ¿De xuru que quies facelo?</string>

View File

@ -1,252 +0,0 @@
<resources>
<!--Author: Radoš Milićev (https://github.com/rammba)-->
<!--Sections-->
<string name="modules">Moduli</string>
<string name="superuser">Super-korisnik</string>
<string name="logs">Logovi</string>
<string name="settings">Podešavanja</string>
<string name="install">Instalacija</string>
<string name="section_home">Početno</string>
<string name="section_theme">Teme</string>
<string name="denylist">Lista zabrana</string>
<!--Home-->
<string name="no_connection">Nedostupna konekcija</string>
<string name="app_changelog">Promene u aplikaciji</string>
<string name="loading">Učitavanje…</string>
<string name="update">Ažuriranje</string>
<string name="not_available">N/A</string>
<string name="hide">Sakrij</string>
<string name="home_package">Paket</string>
<string name="home_app_title">Apl.</string>
<string name="home_notice_content">Preuzmite Magisk SAMO sa zvanične GitHub stranice. Fajlovi iz nepoznatih izvora mogu biti maliciozni!</string>
<string name="home_support_title">Podržite nas</string>
<string name="home_follow_title">Zapratite nas</string>
<string name="home_item_source">Izvor</string>
<string name="home_support_content">Magisk jeste i uvek će biti besplatan i open source. Možete pokazati da vam je stalo svojom donacijom.</string>
<string name="home_installed_version">Instalirano</string>
<string name="home_latest_version">Najnovije</string>
<string name="invalid_update_channel">Nevalidan kanal ažuriranja</string>
<string name="uninstall_magisk_title">Deinstaliraj Magisk</string>
<string name="uninstall_magisk_msg">Svi moduli će biti onemogućeni/uklonjeni!\nKoren će biti uklonjen!\nSvako neenkriptovano interno skladište će upotrebom Magisk-a biti ponovo enkriptovano!</string>
<!--Install-->
<string name="keep_force_encryption">Zadrži forsiranu enkripciju</string>
<string name="keep_dm_verity">Zadrži AVB 2.0/dm-verity</string>
<string name="recovery_mode">Režim oporavka</string>
<string name="install_options_title">Opcije</string>
<string name="install_method_title">Metod</string>
<string name="install_next">Naredno</string>
<string name="install_start">Počnimo</string>
<string name="manager_download_install">Pritisni da preuzmeš i instaliraš</string>
<string name="direct_install">Direktna instalacija (Preporučeno)</string>
<string name="install_inactive_slot">Instalacija na neaktivan slot (Nakon OTA)</string>
<string name="install_inactive_slot_msg">Vaš uređaj će biti FORSIRAN da se pokrene na trenutno neaktivnom slotu nakon ponovnog pokretanja!\nKoristite opciju samo kad se OTA završi.\nNastavi?</string>
<string name="setup_title">Dodatne postavke</string>
<string name="select_patch_file">Izaberite fajl</string>
<string name="patch_file_msg">Izaberite sliku (*.img) ili ODIN tarfile (*.tar) ili payload.bin (*.bin)</string>
<string name="reboot_delay_toast">Ponovo pokretanje za 5 sekundi…</string>
<string name="flash_screen_title">Instalacija</string>
<!--Superuser-->
<string name="su_request_title">Super-korisnički zahtev</string>
<string name="touch_filtered_warning">Magisk ne može da verifikuje vaš odgovor, jer aplikacija prikriva super-korisnički zahtev</string>
<string name="deny">Zabrani</string>
<string name="prompt">Zahtev</string>
<string name="grant">Dozvoli</string>
<string name="su_warning">Pruža potpun pristup vašem uređaju.\nZabranite ako niste sigurni!</string>
<string name="forever">Zauvek</string>
<string name="once">Jednom</string>
<string name="tenmin">10 min</string>
<string name="twentymin">20 min</string>
<string name="thirtymin">30 min</string>
<string name="sixtymin">60 min</string>
<string name="su_allow_toast">%1$s je dobio prava na super-korisnika</string>
<string name="su_deny_toast">%1$s nije dobio prava na super-korisnika</string>
<string name="su_snack_grant">Super-korisnička prava od %1$s su pružena</string>
<string name="su_snack_deny">Super-korisnička prava od %1$s su odbijena</string>
<string name="su_snack_notif_on">Notifikacije od %1$s su omogućene</string>
<string name="su_snack_notif_off">Notifikacije od %1$s su onemogućene</string>
<string name="su_snack_log_on">Logovanje za %1$s je omogućeno</string>
<string name="su_snack_log_off">Logovanje za %1$s je onemogućeno</string>
<string name="su_revoke_title">Opozovi?</string>
<string name="su_revoke_msg">Potvrdi da opozoveš prava na super-korisnika od %1$s?</string>
<string name="toast">Toast</string>
<string name="none">Ništa</string>
<string name="superuser_toggle_notification">Notifikacije</string>
<string name="superuser_toggle_revoke">Opozovi</string>
<string name="superuser_policy_none">Nijedna aplikacija nije tražila permisije za super-korisnika još uvek.</string>
<!--Logs-->
<string name="log_data_none">Nemate logova, pokušajte koristiti korenske aplikacije više</string>
<string name="log_data_magisk_none">Magisk logovi su prazni, to je čudno</string>
<string name="menuSaveLog">Sačuvaj log</string>
<string name="menuClearLog">Ukloni log</string>
<string name="logs_cleared">Log uspešno uklonjen</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">Ciljani UID: %1$d</string>
<string name="target_pid">Mount ns cinjani PID: %s</string>
<string name="selinux_context">SELinux kontekst: %s</string>
<string name="supp_group">Dopunska grupa: %s</string>
<!--SafetyNet-->
<!--MagiskHide-->
<string name="show_system_app">Prikaži sistemske apl.</string>
<string name="show_os_app">Prikaži apl. OS-a</string>
<string name="hide_filter_hint">Filtriraj po imenu</string>
<string name="hide_search">Pretraga</string>
<!--Module-->
<string name="no_info_provided">(Bez informacija)</string>
<string name="reboot_userspace">Lako ponovo pokretanje</string>
<string name="reboot_recovery">Ponovo pokreni za oporavak</string>
<string name="reboot_bootloader">Ponovo pokreni za bootloader</string>
<string name="reboot_download">Ponovo pokreni za preuzimanje</string>
<string name="reboot_edl">Ponovo pokreni za EDL</string>
<string name="reboot_safe_mode">Siguran mod</string>
<string name="module_version_author">%1$s od %2$s</string>
<string name="module_state_remove">Ukloni</string>
<string name="module_action">Akcija</string>
<string name="module_state_restore">Povrati</string>
<string name="module_action_install_external">Instaliraj iz skladišta</string>
<string name="update_available">Ažuriranje dostupno</string>
<string name="suspend_text_riru">Modul je suspendovan jer je %1$s omogućeno</string>
<string name="suspend_text_zygisk">Modul je suspendovan jer %1$s nije omogućeno</string>
<string name="zygisk_module_unloaded">Zygisk modul nije učitan zbog nekompatibilnosti</string>
<string name="module_empty">Nijedan modul nije instaliran</string>
<string name="confirm_install">Instaliraj modul %1$s?</string>
<string name="confirm_install_title">Potvrda instalacije</string>
<!--Settings-->
<string name="settings_dark_mode_title">Tema</string>
<string name="settings_dark_mode_message">Izaberite temu koja vam najviše odgovara!</string>
<string name="settings_dark_mode_light">Uvek svetlo</string>
<string name="settings_dark_mode_system">Prati sistem</string>
<string name="settings_dark_mode_dark">Uvek tamno</string>
<string name="settings_download_path_title">Putanja za preuzimanje</string>
<string name="settings_download_path_message">Fajlovi će biti sačuvani na %1$s</string>
<string name="settings_hide_app_title">Sakrij Magisk apl.</string>
<string name="settings_hide_app_summary">Instaliraj proxy aplikaciju sa nasumičnim ID-jem paketa i prilagođenom labelom</string>
<string name="settings_restore_app_title">Povrati Magisk apl.</string>
<string name="settings_restore_app_summary">Otkrij apl. i povrati originalni APK</string>
<string name="language">Jezik</string>
<string name="system_default">(Podrazumevano sistemski)</string>
<string name="settings_check_update_title">Proveri ažuriranja</string>
<string name="settings_check_update_summary">Periodično proveri ažuriranja u pozadini</string>
<string name="settings_update_channel_title">Kanal ažuriranja</string>
<string name="settings_update_stable">Stabilno</string>
<string name="settings_update_beta">Beta</string>
<string name="settings_update_custom">Prilagođeno</string>
<string name="settings_update_custom_msg">Unesi prilagođeni URL kanala</string>
<string name="settings_zygisk_summary">Pokreni delove Magisk-a u zygote daemon-u</string>
<string name="settings_denylist_title">Sprovedi listu zabrana</string>
<string name="settings_denylist_summary">Procesi na listi zabrana će povratiti sve Magisk izmene</string>
<string name="settings_denylist_config_title">Konfiguriši listu zabrana</string>
<string name="settings_denylist_config_summary">Izaberi procese koji će biti na listi zabrana</string>
<string name="settings_hosts_title">Bezsistemski domaćini (hosts)</string>
<string name="settings_hosts_summary">Podrška bezsistemskih domaćina za aplikacije blokiranja reklama</string>
<string name="settings_hosts_toast">Modul bezsistemskih domaćina dodat</string>
<string name="settings_app_name_hint">Novo ime</string>
<string name="settings_app_name_helper">Apl. će biti spakovana pod ovim imenom</string>
<string name="settings_app_name_error">Nevalidan format</string>
<string name="settings_su_app_adb">Aplikacije i ADB</string>
<string name="settings_su_app">Samo aplikacije</string>
<string name="settings_su_adb">Samo ADB</string>
<string name="settings_su_disable">Onemogućeno</string>
<string name="settings_su_request_10">10 sekundi</string>
<string name="settings_su_request_15">15 sekundi</string>
<string name="settings_su_request_20">20 sekundi</string>
<string name="settings_su_request_30">30 sekundi</string>
<string name="settings_su_request_45">45 sekundi</string>
<string name="settings_su_request_60">60 sekundi</string>
<string name="superuser_access">Pristup super-korisnika</string>
<string name="auto_response">Automatski odgovor</string>
<string name="request_timeout">Istek zahteva</string>
<string name="superuser_notification">Notifikacije super-korisnika</string>
<string name="settings_su_reauth_title">Ponovo odobri nakon ažuriranja</string>
<string name="settings_su_reauth_summary">Ponovo traži permisije super-korisnika nakon ažuriranja aplikacija</string>
<string name="settings_su_tapjack_title">Zaštita od tapjacking-a</string>
<string name="settings_su_tapjack_summary">Prompt dijalog super-korisnika neće reagovati dok je prikriven drugim prozorom ili overlay-em</string>
<string name="settings_su_auth_title">Autentifikacija korisnika</string>
<string name="settings_su_auth_summary">Traži autentifikaciju korisnika tokom zahteva super-korisnika</string>
<string name="settings_su_auth_insecure">Nijedan metod autentifikacije nije podešen na uređaju</string>
<string name="settings_customization">Prilagođavanje</string>
<string name="setting_add_shortcut_summary">Dodaj lepu prečicu na početni ekran u slučaju da se ime i ikonica ne prepoznaju lako nakon skrivanja aplikacije</string>
<string name="settings_doh_title">DNS preko HTTPS-a</string>
<string name="settings_doh_description">Zaobilazno rešenje DNS trovanja u nekim nacijama</string>
<string name="settings_random_name_title">Nasumično ime na izlazu</string>
<string name="settings_random_name_description">Nasumično ime izlaznog fajla slika i tar fajlova radi sprečavanja detekcije</string>
<string name="multiuser_mode">Višekorisnički režim</string>
<string name="settings_owner_only">Samo vlasnik uređaja</string>
<string name="settings_owner_manage">Određeno od strane vlasnika</string>
<string name="settings_user_independent">Nezavisno od korisnika</string>
<string name="owner_only_summary">Samo vlasnik ima pristup korenu</string>
<string name="owner_manage_summary">Samo vlasnik može da pristupa korenu i da prima zahteve za njega</string>
<string name="user_independent_summary">Svaki korisnik ima svoja pravila korena</string>
<string name="mount_namespace_mode">Mount režim namespace-a</string>
<string name="settings_ns_global">Globalni namespace</string>
<string name="settings_ns_requester">Nasleđeni namespace</string>
<string name="settings_ns_isolate">Izolovani namespace</string>
<string name="global_summary">Sve korenske sesije koriste globalni mount namespace</string>
<string name="requester_summary">Korenske sesije će naslediti namespace od podnosioca zahteva</string>
<string name="isolate_summary">Svaka korenska sesija će imati svoj izolovani namespace</string>
<!--Notifications-->
<string name="update_channel">Ažuriranja Magisk-a</string>
<string name="progress_channel">Notifikacije o progresu</string>
<string name="updated_channel">Ažuriranje završeno</string>
<string name="download_complete">Preuzimanje završeno</string>
<string name="download_file_error">Greška pri preuzimanju fajla</string>
<string name="magisk_update_title">Ažuriranje Magisk-a dostupno!</string>
<string name="updated_title">Magisk je ažuriran</string>
<string name="updated_text">Klikni da otvoriš aplikaciju</string>
<!--Toasts, Dialogs-->
<string name="yes">Da</string>
<string name="no">Ne</string>
<string name="repo_install_title">Instaliraj %1$s %2$s(%3$d)</string>
<string name="download">Preuzmi</string>
<string name="reboot">Ponovo pokreni</string>
<string name="close">Zatvori</string>
<string name="release_notes">Release notes</string>
<string name="flashing">Flešovanje…</string>
<string name="running">Pokretanje…</string>
<string name="done">Završeno!</string>
<string name="done_action">Pokretanje akcije %1$s završeno</string>
<string name="failure">Neuspešno!</string>
<string name="hide_app_title">Skrivanje Magisk aplikacije…</string>
<string name="open_link_failed_toast">Nije pronađena aplikacija za otvaranje linka</string>
<string name="complete_uninstall">Kompletna deinstalacija</string>
<string name="restore_img">Povrati slike</string>
<string name="restore_img_msg">Povratak…</string>
<string name="restore_done">Povratak uspešan!</string>
<string name="restore_fail">Fabrički bekap ne postoji!</string>
<string name="setup_fail">Neuspešna postavka</string>
<string name="env_fix_title">Potrebno dodatno podešavanje</string>
<string name="env_fix_msg">Vaš uređaj zahteva dodatno podešavanje da bi Magisk radio kako treba. Da li želite nastaviti i pokrenuti ponovo?</string>
<string name="env_full_fix_msg">Vaš uređaj zahteva ponovno flešovanje da bi Magisk radio kako treba. Reinstalirajte Magisk kroz aplikaciju, režim oporavka ne može dobiti tačne informacije o uređaju.</string>
<string name="setup_msg">Pokretanje podešavanja okruženja…</string>
<string name="unsupport_magisk_title">Nepodržana verzija Magisk-a</string>
<string name="unsupport_magisk_msg">Ova verzija aplikacije ne podržava Magisk verzije manje od %1$s.\n\nAplikacija će se ponašati kao da Magisk nije instaliran, molimo ažurirajte Magisk što pre.</string>
<string name="unsupport_general_title">Nenormalno stanje</string>
<string name="unsupport_system_app_msg">Pokretanje aplikacije kao sistemske nije podržano. Molimo postavite aplikaciju da bude korisnička.</string>
<string name="unsupport_other_su_msg">Detektovan \"su\" binary koji nije Magisk-ov. Molimo uklonite konkurentno korensko rešenje i/ili reinstalirajte Magisk.</string>
<string name="unsupport_external_storage_msg">Magisk je instaliran na eksterno skladište. Molimo pomerite apl. u interno skladište.</string>
<string name="unsupport_nonroot_stub_msg">Skrivena Magisk aplikacija ne može nastaviti sa radom jer je koren izgubljen. Molimo povratite originalni APK.</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">Dozvolite permisiju za skladište da biste omogućili ovu funkcionalnost</string>
<string name="post_notifications_denied">Dozvolite permisiju za notifikacije da biste omogućili ovu funkcionalnost</string>
<string name="install_unknown_denied">Dozvolite "instaliranje nepoznatih aplikacija" da biste omogućili ovu funkcionalnost</string>
<string name="add_shortcut_title">Dodaj prečicu na početni ekran</string>
<string name="add_shortcut_msg">Nakon skrivanja aplikacije, njeno ime i ikonicu ćete teško prepoznati. Želite li dodati lepu prečicu na početni ekran?</string>
<string name="app_not_found">Nije pronađena aplikacija za ovu akciju</string>
<string name="reboot_apply_change">Ponovo pokreni da primeniš izmene</string>
<string name="restore_app_confirmation">Ovo će vratiti skrivenu aplikaciju na originalnu. Da li stvarno to želite?</string>
</resources>

View File

@ -1,252 +0,0 @@
<resources>
<!--Sections-->
<string name="modules">زیادکراوەکان</string>
<string name="superuser">سوپەر یوسەر</string>
<string name="logs">تۆمارەکان</string>
<string name="settings">ڕێکخستنەکان</string>
<string name="install">دامەزراندن</string>
<string name="section_home">ماڵەوە</string>
<string name="section_theme">ڕووکارەکان</string>
<string name="denylist">پێڕستی ڕێگەپێنەدراوەکان</string>
<!--Home-->
<string name="no_connection">هێڵ بەردەست نییە</string>
<string name="app_changelog">گۆڕانکارییەکان</string>
<string name="loading">کردنەوە…</string>
<string name="update">بەرزکردنەوە</string>
<string name="not_available">نییە</string>
<string name="hide">شاردنەوە</string>
<string name="home_package">پاکێج</string>
<string name="home_app_title">ئەپ</string>
<string name="home_notice_content">تەنها لە گیتهەبی فەرمی ماجیسک دابگرە، لە شوێنی تر لەوانەیە زیانبەخش بێت</string>
<string name="home_support_title">پشتگیریمان بکە</string>
<string name="home_follow_title">شوێنمان بکەوە</string>
<string name="home_item_source">سەرچاوە</string>
<string name="home_support_content">ماجیسک بە خۆڕاییە و هەر واش ئەمێنێتەوە، بەهەرحاڵ ئەتوانیت پشتگیرییەکمان بکەی بۆ گرنگی پێدان</string>
<string name="home_installed_version">داگیراوە</string>
<string name="home_latest_version">دوایین وەشان</string>
<string name="invalid_update_channel">کەناڵێکی نوێکردنەوەی هەڵە</string>
<string name="uninstall_magisk_title">سڕینەوەی ماجیسک</string>
<string name="uninstall_magisk_msg">هەموو زیادکراوەکان دەسڕێنەوە، ڕۆت دەسڕێتەوە، و هەر ڕەمزێنراوێک بەهۆی ماجیسک کرابێ لادەچێت!</string>
<!--Install-->
<string name="keep_force_encryption">ڕەمزاندنی بەزۆر بهێڵەوە</string>
<string name="keep_dm_verity">بهێڵەوە AVB 2.0/dm-verity</string>
<string name="recovery_mode">دۆخی ڕیکەڤەڕی</string>
<string name="install_options_title">هەڵبژاردنەکان</string>
<string name="install_method_title">ڕێگای</string>
<string name="install_next">دواتر</string>
<string name="install_start">با بیکەین</string>
<string name="manager_download_install">بۆ داگرتن و ڕێکخستن کرتە بکە</string>
<string name="direct_install">داگرتنی ڕاستەوخۆ(پێشنیارکراوە)</string>
<string name="install_inactive_slot">دایبگرە بۆ خانەی ناچالاک(پاش OTA)</string>
<string name="install_inactive_slot_msg">ئێستا ئامێرەکەت دەچێتە خانە ناچالاکەکە، ئەمە بەکاربهێنە تەنها دوای ئەپدەیت کردن لە ڕێگەی OTA، بەردەوام دەبیت؟</string>
<string name="setup_title">ڕێکخستنی زیاتر</string>
<string name="select_patch_file">فایلێک هەڵبژێرە و پینەی بکە</string>
<string name="patch_file_msg">تکایە فایلێکی Tar یان img یان payload.bin هەڵبژێرە</string>
<string name="reboot_delay_toast">ڕێستارت کردنەوە لە ماوەی ٥ چرکە…</string>
<string name="flash_screen_title">ڕێکخستن</string>
<!--Superuser-->
<string name="su_request_title">داواکاری سوپەریوسەر</string>
<string name="touch_filtered_warning">ئەپێک لە سەر شاشەکەیە، ناتوانین دڵنیا بینەوە</string>
<string name="deny">ڕەتکردنەوە</string>
<string name="prompt">داواکاری</string>
<string name="grant">ڕێگەپێدان</string>
<string name="su_warning">ڕێگەپێدان بۆ تەواوی ئامێرەکەت، گەر دڵنیا نیت ڕەتی بکەوە</string>
<string name="forever">بۆ هەمیشە</string>
<string name="once">بۆ یەکجار</string>
<string name="tenmin">بۆ ١٠ خولەک</string>
<string name="twentymin">بۆ ٢٠ خولەک</string>
<string name="thirtymin">بۆ ٣٠ خولەک</string>
<string name="sixtymin">بۆ ٦٠ خولەک</string>
<string name="su_allow_toast">%1$s ڕێگەپێدانی سوپەریوسەری بۆ زیادکرا</string>
<string name="su_deny_toast">%1$s ڕێگەپێدانی سوپەریوسەر ڕەتکرایەوە</string>
<string name="su_snack_grant">ڕێگەپێدانی سوپەریوسەری %1$s بۆ درا</string>
<string name="su_snack_deny">ڕێگەپێدانی سوپەریوسەر %1$s ڕەتکرایەوە</string>
<string name="su_snack_notif_on">ئاگەدارکردنەوەکانی %1$s کارا کراوە</string>
<string name="su_snack_notif_off">ئاگەدارکردنەوەکانی %1$s کوژاوەتەوە</string>
<string name="su_snack_log_on">تۆمارەکانی %1$s کراوەتەوە</string>
<string name="su_snack_log_off">تۆمارەکانی %1$s کوژاوەتەوە</string>
<string name="su_revoke_title">لابردن؟</string>
<string name="su_revoke_msg">دڵنیابەوە بۆ لابردنی سوپەریوسەر بۆ %1$s </string>
<string name="toast">هێنانەسەر</string>
<string name="none">هیچ</string>
<string name="superuser_toggle_notification">ئاگادارییەکان</string>
<string name="superuser_toggle_revoke">لابردن</string>
<string name="superuser_policy_none">هیچ ئەپێک تا ئێستا داوای سوپەریوسەری نەکردووە</string>
<!--Logs-->
<string name="log_data_none">هیچ تۆمارێک نییە، ئەو ئەپانەی ڕۆتیان پێویستە زوزو بەکاریبێنە</string>
<string name="log_data_magisk_none">تۆمارەکانی ماجیسک بەتاڵن، باشە بۆ؟</string>
<string name="menuSaveLog">تۆمارەکان هەڵبگرە</string>
<string name="menuClearLog">تۆمارەکان بسڕەوە</string>
<string name="logs_cleared">بەسەرکەوتویی تۆمارەکان سڕانەوە</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">ئامانج UID: %1$d</string>
<string name="target_pid">Mount ns target PID: %s</string>
<string name="selinux_context">SELinux context: %s</string>
<string name="supp_group">Supplementary group: %s</string>
<!--SafetyNet-->
<!--MagiskHide-->
<string name="show_system_app">پیشاندانی ئەپەکانی سیستەم</string>
<string name="show_os_app">پیشاندانی ئەپەکان</string>
<string name="hide_filter_hint">پاڵاوتنی بەپێی ناو</string>
<string name="hide_search">گەڕان</string>
<!--Module-->
<string name="no_info_provided">(هیچ زانیارییەک نییە)</string>
<string name="reboot_userspace">ڕێستارت کردنەوە</string>
<string name="reboot_recovery">چوونە ناو ڕیکەڤەری</string>
<string name="reboot_bootloader">چوونە ناو بووتلۆدەر</string>
<string name="reboot_download">چوونە ناو داونلۆد</string>
<string name="reboot_edl">چوونە ناو EDL</string>
<string name="reboot_safe_mode">دۆخی پارێزراو</string>
<string name="module_version_author">%1$s by %2$s</string>
<string name="module_state_remove">سڕینەوە</string>
<string name="module_action">کارا</string>
<string name="module_state_restore">گەڕاندنەوە</string>
<string name="module_action_install_external">لە بیرگەکەتەوە ڕێکی بخە</string>
<string name="update_available">وەشانی نوێ بەردەستە</string>
<string name="suspend_text_riru">زیادکراوەکە کار ناکات چونکە %1$s کراوەتەوە</string>
<string name="suspend_text_zygisk">زیادکراوەکە کارناکات چونکە %1$s نەکراوەتەوە</string>
<string name="zygisk_module_unloaded">زیادکراوی Zygisk بەهۆی نەگونجان کارناکات</string>
<string name="module_empty">هیچ زیادکراوێک دانەبەزیوە</string>
<string name="confirm_install">دابەزاندنی زیادکراو %1$s?</string>
<string name="confirm_install_title">دڵنیابوونەوە لە دابەزاندن</string>
<!--Settings-->
<string name="settings_dark_mode_title">جۆری ڕووکار</string>
<string name="settings_dark_mode_message">حەزت لە کامەی بوو ئەوە هەڵبژێرە</string>
<string name="settings_dark_mode_light">هەمیشە دۆخی ڕوناک</string>
<string name="settings_dark_mode_system">با بەگوێرەی سیستەمەکە بێت!</string>
<string name="settings_dark_mode_dark">هەمیشە دۆخی تاریک</string>
<string name="settings_download_path_title">شوێنی داگرتنەکە</string>
<string name="settings_download_path_message">فایلەکان هەڵدەگیرێن لە %1$s</string>
<string name="settings_hide_app_title">شاردنەوەی ئەپی ماجیسک</string>
<string name="settings_hide_app_summary">داگرتنی ماجیسک بە ناوی جیاوە</string>
<string name="settings_restore_app_title">ئەپە ڕەسەنەکە بهێنەوە</string>
<string name="settings_restore_app_summary">ئەپەکە دەربخەوە و ڕەسەنڵ</string>
<string name="language">زمان</string>
<string name="system_default">(وەک هی ئامێرەکە)</string>
<string name="settings_check_update_title">گەڕان بەدوای نوێکاری</string>
<string name="settings_check_update_summary">گەڕان بەدوای نوێکاری خۆکارانە</string>
<string name="settings_update_channel_title">کەناڵی نوێکاری</string>
<string name="settings_update_stable">جێگیر</string>
<string name="settings_update_beta">پێشوەختە(بێتا)</string>
<string name="settings_update_custom">تایبەت</string>
<string name="settings_update_custom_msg">بەستەرێکی تایبەت دابنێ</string>
<string name="settings_zygisk_summary">کارپێکردنی بەشێکی ماجیسک لە zygote daemon</string>
<string name="settings_denylist_title">پێڕستی نەرێنی کراوەکان</string>
<string name="settings_denylist_summary">هەر ئەپێک لە پێڕستی نەرێنییەکان کاریگەریەکانی ماجیسکی لەسەر نییە</string>
<string name="settings_denylist_config_title">دەستکاریکردنی پێڕستی نەرێنیکراوەکان</string>
<string name="settings_denylist_config_summary">ئەو ئەپە هەڵبژێرە کە دەتەوێت نەرێنیی بکەیت</string>
<string name="settings_hosts_title">هۆستی ناسیستەمی</string>
<string name="settings_hosts_summary"> هۆستی ناسیستەمی بۆ لابردنی ڕیکلامەکان</string>
<string name="settings_hosts_toast"> هۆستی ناسیستەمی زیادکرا</string>
<string name="settings_app_name_hint">ناوی نوێ</string>
<string name="settings_app_name_helper">ئەپەکە بەم ناوەوە دروست دەکرێتەوە</string>
<string name="settings_app_name_error">هەڵەیە</string>
<string name="settings_su_app_adb">ئەپەکان و ADB</string>
<string name="settings_su_app">تەنها ئەپەکان</string>
<string name="settings_su_adb">ADB تەنها</string>
<string name="settings_su_disable">ناچالاک کراوە</string>
<string name="settings_su_request_10">10 چرکە</string>
<string name="settings_su_request_15">15 چرکە</string>
<string name="settings_su_request_20">20 چرکە</string>
<string name="settings_su_request_30">30 چرکە</string>
<string name="settings_su_request_45">45 چرکە</string>
<string name="settings_su_request_60">60 چرکە</string>
<string name="superuser_access">دەسەڵاتی سوپەریوسەر</string>
<string name="auto_response">وەڵامدانەوەی خۆکارانە</string>
<string name="request_timeout">ماوەی وەڵامدانەوە</string>
<string name="superuser_notification">ئاگەدارییەکانی سوپەریوسەر</string>
<string name="settings_su_reauth_title">پرسیاربکەوە دوای هەر نوێکردنەوەیەک</string>
<string name="settings_su_reauth_summary">دوای نوێکردنەوەی ئەپەکان دووبارە پرسیار بکەوە بۆ دەسەڵاتی سوپەریوسەر</string>
<string name="settings_su_tapjack_title">پارێزگاری کردن لە ئەگەری دەستلێدانی تر</string>
<string name="settings_su_tapjack_summary"> کاتێک ئەپێکی تر بەسەر شاشەکەوەیە، سوپەر یوسەر وەڵام ناداتەوە لە کردنی هەر بژاردەیەک لەبەر پارێزراوی</string>
<string name="settings_su_auth_title">دڵنیاکردنەوەی کەسی</string>
<string name="settings_su_auth_summary">داواکاری بکە بۆ دڵنیاکردنەوەی کەسی لەکاتی داواکاری سوپەریوسەر</string>
<string name="settings_su_auth_insecure">هیچ دڵنیاکردنەوەیەک نییە</string>
<string name="settings_customization">دەستکاریکردن</string>
<string name="setting_add_shortcut_summary">یەک ئایکۆنی جوان زیادبکە بۆ سەر شاشەکە ئەگەر قورس بوو ئەوەی خۆی بدۆزیتەوە</string>
<string name="settings_doh_title">DNS بەسەر HTTPS</string>
<string name="settings_doh_description">Workaround DNS خراپە لە هەندێک شوێن</string>
<string name="settings_random_name_title">ناوێک لەخۆیەوە</string>
<string name="settings_random_name_description">دانانی ناوێک لەخۆوە تاوەکوو ئاشکرا نەبێت</string>
<string name="multiuser_mode">دۆخی فرەبەکارهێنەر</string>
<string name="settings_owner_only">تەنها خاوەنی ئامێر</string>
<string name="settings_owner_manage">خاوەنی ئامێر</string>
<string name="settings_user_independent">بەکارهێنەری سەربەخۆ</string>
<string name="owner_only_summary">تەنها خاوەنەکە دۆخی ڕۆتی هەیە</string>
<string name="owner_manage_summary">تەنها خاوەنەکە دەسەڵاتی بەکارهێنانی ڕۆتی هەیە</string>
<string name="user_independent_summary">هەر بەکارهێنەرێک یاسای جیاوازی هەیە</string>
<string name="mount_namespace_mode">چونە دۆخی بۆشایی ناو</string>
<string name="settings_ns_global">بۆشاییناوی گشتی</string>
<string name="settings_ns_requester">بۆشایی ناوی خۆیی</string>
<string name="settings_ns_isolate">بۆشایی ناوی جیا</string>
<string name="global_summary">هەمو ڕۆتەکان ناوی گشتی بەکار ئەهێنن</string>
<string name="requester_summary">هەمو ڕۆتەکان ناوی خۆیی بەکار ئەهێنن</string>
<string name="isolate_summary">هەمو ڕۆتەکان ناوی جیا بەکار ئەهێنن</string>
<!--Notifications-->
<string name="update_channel">نوێکردنەوەکانی ماجیسک</string>
<string name="progress_channel">ئاگادارییە کاراکان</string>
<string name="updated_channel">نوێکردنەوە سەرکەوتووبوو</string>
<string name="download_complete">داگرتن سەرکەوتووبوو</string>
<string name="download_file_error">هەڵەیەک رووی دا لەکاتی داگرتنی فایلەکە</string>
<string name="magisk_update_title">وەشانی نوێی ماجیسک ئامادەیە!</string>
<string name="updated_title">ماجیسک نوێکراوە!</string>
<string name="updated_text">کرتە بکە بۆ کردنەوەی ئەپ</string>
<!--Toasts, Dialogs-->
<string name="yes">بەڵێ</string>
<string name="no">نەخێر</string>
<string name="repo_install_title">داگرتن %1$s %2$s(%3$d)</string>
<string name="download">داگرتن</string>
<string name="reboot">ڕێستارت</string>
<string name="close">داخستن</string>
<string name="release_notes">نێبینییەکان</string>
<string name="flashing">فلاش کردن</string>
<string name="running">کار کردن...</string>
<string name="done">تەواو!</string>
<string name="done_action">کارکردنی %1$s تەواو بوو</string>
<string name="failure">Failed!</string>
<string name="hide_app_title">شاردنەوەی ئەپی ماجیسک…</string>
<string name="open_link_failed_toast">هیچ ئەپێک تییە تا لینکەکەی پێ بکرێتەوە</string>
<string name="complete_uninstall">سڕینەوەی تەواوی</string>
<string name="restore_img">هێنانەوەی img</string>
<string name="restore_img_msg">هێنانەوە…</string>
<string name="restore_done">هاتەوە!</string>
<string name="restore_fail">هیچ فایلێکی هەڵگیراوت نیە!</string>
<string name="setup_fail">ڕێکخستن شکستی هێنا</string>
<string name="env_fix_title">پێویستی بە ڕێکخستنی زیاترە</string>
<string name="env_fix_msg">مۆبایلەکەت پێویستی بە ڕێکخستنی زیاترە، ئایا ئەتەوێت بەردەوام بیت و ڕێستارتی بکەیتەوە؟</string>
<string name="env_full_fix_msg"> پێویستە دوبارە ماجیسک دابگریتەوە بۆ ئەوەی بەباشی کاربکات تکایە ماجیسک دابگرەوە لەناو ئەپەکە خۆی چونکە لە ڕیکەڤەرییەوە ناتوانرێ زانیاری تەواو لەسەر ئامێرەکە دەستبخرێت </string>
<string name="setup_msg">دەستپێکردن....</string>
<string name="unsupport_magisk_title">وەشانی ماجیسک پاڵپشتینەکراوە</string>
<string name="unsupport_magisk_msg">وەشانی ماجیسکەکەت زۆر کۆنە وەک ئەوە وایە هەر نەبێت، تکایە نوێی بکەوە بە زوترین کات</string>
<string name="unsupport_general_title">باری نائاسایی</string>
<string name="unsupport_system_app_msg">ئەم ئەپە وەکو ئەپی سیستەم کارناکات، تکایە بیگۆڕەوە بۆ ئەپی ئاسایی</string>
<string name="unsupport_other_su_msg"> \"su\" binary یەکی بێگانە دۆزرایەوە، تکایە جگە لە ماجیسک ئەپی تر بەکارمەهێنە بۆ ڕۆت کردن </string>
<string name="unsupport_external_storage_msg">ماجیسک لە بیرگەی دەرەکی داگیراوە، تکایە بیبەوە بۆ ناوەکی</string>
<string name="unsupport_nonroot_stub_msg">ئەپە شاراوەکە کار ناکات چونکە ڕۆتەکە نەماوە، تکایە ئەپە ڕەسەنەکە بگەڕێنەوە</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">ڕەزامەندی بیرگە بدە تاوەکوو ئەمە کار بکات</string>
<string name="post_notifications_denied">ڕەزامەندی ئاگاداری بکە تاوەکوو ئەمە کار بکات</string>
<string name="install_unknown_denied">ڕەزامەندی "install unknown apps" تاوەکوو کار بکات</string>
<string name="add_shortcut_title">زیادی بکە بۆ سەر شاشە</string>
<string name="add_shortcut_msg">دوای شاردنەوەی ئەم ئەپە، ئەتەوێت یەک ئایکۆنی جوان زیادبکەیت بۆ سەر شاشەکە ئەگەر قورس بوو ئەوەی خۆی بدۆزیتەوە؟</string>
<string name="app_not_found">هیچ ئەپێک نەدۆزرایەوە تاوەکوو ئەم کارەی پێ بکرێت</string>
<string name="reboot_apply_change">ڕێستارت بکە تاوەکوو کاریگەریەکان کار بکەن</string>
<string name="restore_app_confirmation">ئەمە ئەپە ڕەسەنەکە ئەهێنێتەوە، دڵنیایت لە کردنی؟</string>
</resources>

View File

@ -1,58 +1,99 @@
<resources> <resources>
<!--Author: Radoš Milićev (https://github.com/rammba)--> <!--Universal-->
<!--Sections--> <!--Welcome Activity-->
<string name="modules">Модули</string> <string name="modules">Модули</string>
<string name="superuser">Супер-корисник</string> <string name="superuser">Супер-корисник</string>
<string name="logs">Логови</string> <string name="logs">Дневник</string>
<string name="settings">Подешавања</string> <string name="settings">Подешавања</string>
<string name="install">Инсталација</string> <string name="install">Инсталација</string>
<string name="section_home">Почетно</string>
<string name="section_theme">Теме</string>
<string name="denylist">Листа забрана</string>
<!--Home--> <!--Status Fragment-->
<string name="no_connection">Недоступна конекција</string> <string name="invalid_update_channel">Непостојећи Канал Ажурирања</string>
<string name="app_changelog">Промене у апликацији</string>
<string name="loading">Учитавање…</string>
<string name="update">Ажурирање</string>
<string name="not_available">N/A</string>
<string name="hide">Сакриј</string>
<string name="home_package">Пакет</string>
<string name="home_app_title">Апл.</string>
<string name="home_notice_content">Преузмите Magisk САМО са званичне GitHub странице. Фајлови из непознатих извора могу бити малициозни!</string> <!--Install Fragment-->
<string name="home_support_title">Подржите нас</string>
<string name="home_follow_title">Запратите нас</string>
<string name="home_item_source">Извор</string>
<string name="home_support_content">Magisk јесте и увек ће бити бесплатан и open source. Можете показати да вам је стало својом донацијом.</string>
<string name="home_installed_version">Инсталирано</string>
<string name="home_latest_version">Најновије</string>
<string name="invalid_update_channel">Невалидан канал ажурирања</string>
<string name="uninstall_magisk_title">Деинсталирај Magisk</string>
<string name="uninstall_magisk_msg">Сви модули ће бити онемогућени/уклоњени!\nКорен ће бити уклоњен!\nСвако неенкриптовано интерно складиште ће употребом Magisk-а бити поново енкриптовано!</string>
<!--Install-->
<string name="keep_force_encryption">Задржи форсирану енкрипцију</string> <string name="keep_force_encryption">Задржи форсирану енкрипцију</string>
<string name="keep_dm_verity">Задржи AVB 2.0/dm-verity</string> <string name="keep_dm_verity">Задржи AVB 2.0/dm-verity</string>
<string name="recovery_mode">Режим опоравка</string> <string name="uninstall_magisk_title">Унинсталирај Магиск</string>
<string name="install_options_title">Опције</string> <string name="uninstall_magisk_msg">Сви модули ће бити онеспособљени/уклоњени. Корен ће бити уклоњен, и потенцијално енкриптовати твоје податке уколико већ нису енкриптовани</string>
<string name="install_method_title">Метод</string> <string name="update">Ажурирање</string>
<string name="install_next">Наредно</string>
<string name="install_start">Почнимо</string> <!--Module Fragment-->
<string name="no_info_provided">(Без информација)</string>
<!--Repo Fragment-->
<string name="update_available">Ажурирање доступно</string>
<string name="home_installed_version">Инсталирано</string>
<!--Log Fragment-->
<string name="menuSaveLog">Сачувај дневник</string>
<string name="menuClearLog">Обриши дневник</string>
<string name="logs_cleared">Дневник успешно креиран</string>
<!--About Activity-->
<string name="app_changelog">Дневник промена апликације</string>
<!--Toasts, Dialogs-->
<string name="repo_install_title">Инсталирај %1$s %2$s(%3$d)</string>
<string name="download">Преузми</string>
<string name="reboot">Рестартуј</string>
<string name="magisk_update_title">Нови Адбејт Магиска Доступан!</string>
<string name="release_notes">Белешке обљављивања</string>
<string name="manager_download_install">Притисни да преузмеш и инсталираш</string> <string name="manager_download_install">Притисни да преузмеш и инсталираш</string>
<string name="direct_install">Директна инсталација (Препоручено)</string> <string name="update_channel">Магиск Ажурирање</string>
<string name="install_inactive_slot">Инсталација на неактиван слот (Након OTA)</string> <string name="flashing">Флешовање</string>
<string name="install_inactive_slot_msg">Ваш уређај ће бити ФОРСИРАН да се покрене на тренутно неактивном слоту након поновног покретања!\nКористите опцију само кад се OTA заврши.\nНастави?</string> <string name="direct_install">Директна Инсталација (Препоручено)</string>
<string name="setup_title">Додатне поставке</string> <string name="complete_uninstall">Комплетна Унинсталација</string>
<string name="select_patch_file">Изаберите фајл</string> <string name="restore_done">Повратак успешан!</string>
<string name="patch_file_msg">Изаберите слику (*.img) или ODIN tarfile (*.tar) или payload.bin (*.bin)</string> <string name="restore_fail">Фабрички бекап не постоји!</string>
<string name="reboot_delay_toast">Поново покретање за 5 секунди…</string> <string name="download_file_error">Грешка при преузимању фајла</string>
<string name="flash_screen_title">Инсталација</string>
<!--Settings Activity -->
<string name="language">Језик</string>
<string name="system_default">(Фабрички Система)</string>
<string name="settings_update_channel_title">Канал Ажурирања</string>
<string name="settings_update_stable">Стабилан</string>
<string name="settings_update_beta">Бета</string>
<string name="settings_update_custom">По наруџби</string>
<string name="settings_update_custom_msg">Унеси наруџбени УРЛ</string>
<string name="settings_hosts_title">Без-системски домаћини (hosts)</string>
<string name="settings_hosts_summary">Подршка без-системских домаћина за апликације за блокирање реклама</string>
<string name="settings_su_app_adb">Апликације и АДБ</string>
<string name="settings_su_app">Само Апликације</string>
<string name="settings_su_adb">Само АДБ</string>
<string name="settings_su_disable">Онемогућено</string>
<string name="settings_su_request_10">10 секунди</string>
<string name="settings_su_request_15">15 секунди</string>
<string name="settings_su_request_20">20 секунди</string>
<string name="settings_su_request_30">30 секунди</string>
<string name="settings_su_request_45">45 секунди</string>
<string name="settings_su_request_60">60 секунди</string>
<string name="superuser_access">Приступ Супер-кориснику</string>
<string name="auto_response">Аутоматски одговор</string>
<string name="request_timeout">Истек захтева</string>
<string name="superuser_notification">Нотификације Супер-корисника</string>
<string name="settings_su_reauth_title">Поново одобри после ажурирања</string>
<string name="settings_su_reauth_summary">Поново одобри дозволу Супер-корисника после ажурирања апликације</string>
<string name="multiuser_mode">Више-кориснички режим</string>
<string name="settings_owner_only">Власник уређаја само</string>
<string name="settings_owner_manage">Одређено од стране власника</string>
<string name="settings_user_independent">Независно од корисника</string>
<string name="owner_only_summary">Само власник има приступ корену</string>
<string name="owner_manage_summary">Само власник може да приступа корену и да прими захтеве за њега</string>
<string name="user_independent_summary">Сваки корисник има своја правила корена</string>
<string name="mount_namespace_mode">Постоље режима имена простора</string>
<string name="settings_ns_global">Глобално име простора</string>
<string name="settings_ns_requester">Наслеђено име простора</string>
<string name="settings_ns_isolate">Ограђено име простора</string>
<string name="global_summary">Све коренске сесије користе глобално име простора</string>
<string name="requester_summary">Коренске сесије ће наследити свој простор именовања</string>
<string name="isolate_summary">Свака коренска сесија ће имати свој изоловани простор именовања</string>
<!--Superuser--> <!--Superuser-->
<string name="su_request_title">Супер-кориснички захтев</string> <string name="su_request_title">Супер-кориснички захтев</string>
<string name="touch_filtered_warning">Magisk не може да верификује ваш одговор, јер апликација прикрива супер-кориснички захтев</string>
<string name="deny">Забрани</string> <string name="deny">Забрани</string>
<string name="prompt">Захтев</string> <string name="prompt">Захтев</string>
<string name="grant">Дозволи</string> <string name="grant">Дозволи</string>
@ -63,190 +104,20 @@
<string name="twentymin">20 мин</string> <string name="twentymin">20 мин</string>
<string name="thirtymin">30 мин</string> <string name="thirtymin">30 мин</string>
<string name="sixtymin">60 мин</string> <string name="sixtymin">60 мин</string>
<string name="su_allow_toast">%1$s је добио права на супер-корисника</string> <string name="su_allow_toast">%1$s је добио права на Супер-корисника</string>
<string name="su_deny_toast">%1$s није добио права на супер-корисника</string> <string name="su_deny_toast">%1$s није добио права на Супер-корисника</string>
<string name="su_snack_grant">Супер-корисничка права од %1$s су пружена</string> <string name="su_snack_grant">Супер-корисничка права од %1$s су пружена</string>
<string name="su_snack_deny">Супер-корисничка права од %1$s су одбијена</string> <string name="su_snack_deny">Супер-корисничка права од %1$s су одбијена</string>
<string name="su_snack_notif_on">Нотификације од %1$s су омогућене</string> <string name="su_snack_notif_on">Нотификације од %1$s су омогућене</string>
<string name="su_snack_notif_off">Нотификације од %1$s су онемогућене</string> <string name="su_snack_notif_off">Нотификације од %1$s су онемогућене</string>
<string name="su_snack_log_on">Логовање за %1$s је омогућено</string> <string name="su_snack_log_on">Записивање у дневник за %1$s је омогућено</string>
<string name="su_snack_log_off">Логовање за %1$s је онемогућено</string> <string name="su_snack_log_off">Записивање у дневник за %1$s је онемогућено</string>
<string name="su_revoke_title">Опозови?</string> <string name="su_revoke_title">Опозови?</string>
<string name="su_revoke_msg">Потврди да опозовеш права на супер-корисника од %1$s?</string> <string name="su_revoke_msg">Потврди да опозовеш права од %1$s?</string>
<string name="toast">Toast</string> <string name="toast">Тост</string>
<string name="none">Ништа</string> <string name="none">Ниједан</string>
<string name="superuser_toggle_notification">Нотификације</string> <!--Superuser logs-->
<string name="superuser_toggle_revoke">Опозови</string> <string name="target_uid">Циљани УИД: %1$d</string>
<string name="superuser_policy_none">Ниједна апликација није тражила пермисије за супер-корисника још увек.</string>
<!--Logs-->
<string name="log_data_none">Немате логова, покушајте користити коренске апликације више</string>
<string name="log_data_magisk_none">Magisk логови су празни, то је чудно</string>
<string name="menuSaveLog">Сачувај лог</string>
<string name="menuClearLog">Уклони лог</string>
<string name="logs_cleared">Лог успешно уклоњен</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">Циљани UID: %1$d</string>
<string name="target_pid">Mount ns цињани PID: %s</string>
<string name="selinux_context">SELinux контекст: %s</string>
<string name="supp_group">Допунска група: %s</string>
<!--SafetyNet-->
<!--MagiskHide-->
<string name="show_system_app">Прикажи системске апл.</string>
<string name="show_os_app">Прикажи апл. ОС-а</string>
<string name="hide_filter_hint">Филтрирај по имену</string>
<string name="hide_search">Претрага</string>
<!--Module-->
<string name="no_info_provided">(Без информација)</string>
<string name="reboot_userspace">Лако поново покретање</string>
<string name="reboot_recovery">Поново покрени за опоравак</string>
<string name="reboot_bootloader">Поново покрени за bootloader</string>
<string name="reboot_download">Поново покрени за преузимање</string>
<string name="reboot_edl">Поново покрени за EDL</string>
<string name="reboot_safe_mode">Сигуран мод</string>
<string name="module_version_author">%1$s од %2$s</string>
<string name="module_state_remove">Уклони</string>
<string name="module_action">Акција</string>
<string name="module_state_restore">Поврати</string>
<string name="module_action_install_external">Инсталирај из складишта</string>
<string name="update_available">Ажурирање доступно</string>
<string name="suspend_text_riru">Модул је суспендован јер је %1$s омогућено</string>
<string name="suspend_text_zygisk">Модул је суспендован јер %1$s није омогућено</string>
<string name="zygisk_module_unloaded">Zygisk модул није учитан због некомпатибилности</string>
<string name="module_empty">Ниједан модул није инсталиран</string>
<string name="confirm_install">Инсталирај модул %1$s?</string>
<string name="confirm_install_title">Потврда инсталације</string>
<!--Settings-->
<string name="settings_dark_mode_title">Тема</string>
<string name="settings_dark_mode_message">Изаберите тему која вам највише одговара!</string>
<string name="settings_dark_mode_light">Увек светло</string>
<string name="settings_dark_mode_system">Прати систем</string>
<string name="settings_dark_mode_dark">Увек тамно</string>
<string name="settings_download_path_title">Путања за преузимање</string>
<string name="settings_download_path_message">Фајлови ће бити сачувани на %1$s</string>
<string name="settings_hide_app_title">Сакриј Magisk апл.</string>
<string name="settings_hide_app_summary">Инсталирај proxy апликацију са насумичним ID-јем пакета и прилагођеном лабелом</string>
<string name="settings_restore_app_title">Поврати Magisk апл.</string>
<string name="settings_restore_app_summary">Откриј апл. и поврати оригинални APK</string>
<string name="language">Језик</string>
<string name="system_default">(Подразумевано системски)</string>
<string name="settings_check_update_title">Провери ажурирања</string>
<string name="settings_check_update_summary">Периодично провери ажурирања у позадини</string>
<string name="settings_update_channel_title">Канал ажурирања</string>
<string name="settings_update_stable">Стабилно</string>
<string name="settings_update_beta">Бета</string>
<string name="settings_update_custom">Прилагођено</string>
<string name="settings_update_custom_msg">Унеси прилагођени URL канала</string>
<string name="settings_zygisk_summary">Покрени делове Magisk-а у zygote daemon-у</string>
<string name="settings_denylist_title">Спроведи листу забрана</string>
<string name="settings_denylist_summary">Процеси на листи забрана ће повратити све Magisk измене</string>
<string name="settings_denylist_config_title">Конфигуриши листу забрана</string>
<string name="settings_denylist_config_summary">Изабери процесе који ће бити на листи забрана</string>
<string name="settings_hosts_title">Безсистемски домаћини (hosts)</string>
<string name="settings_hosts_summary">Подршка безсистемских домаћина за апликације блокирања реклама</string>
<string name="settings_hosts_toast">Модул безсистемских домаћина додат</string>
<string name="settings_app_name_hint">Ново име</string>
<string name="settings_app_name_helper">Апл. ће бити спакована под овим именом</string>
<string name="settings_app_name_error">Невалидан формат</string>
<string name="settings_su_app_adb">Апликације и ADB</string>
<string name="settings_su_app">Само апликације</string>
<string name="settings_su_adb">Само ADB</string>
<string name="settings_su_disable">Онемогућено</string>
<string name="settings_su_request_10">10 секунди</string>
<string name="settings_su_request_15">15 секунди</string>
<string name="settings_su_request_20">20 секунди</string>
<string name="settings_su_request_30">30 секунди</string>
<string name="settings_su_request_45">45 секунди</string>
<string name="settings_su_request_60">60 секунди</string>
<string name="superuser_access">Приступ супер-корисника</string>
<string name="auto_response">Аутоматски одговор</string>
<string name="request_timeout">Истек захтева</string>
<string name="superuser_notification">Нотификације супер-корисника</string>
<string name="settings_su_reauth_title">Поново одобри након ажурирања</string>
<string name="settings_su_reauth_summary">Поново тражи пермисије супер-корисника након ажурирања апликација</string>
<string name="settings_su_tapjack_title">Заштита од tapjacking-а</string>
<string name="settings_su_tapjack_summary">Prompt дијалог супер-корисника неће реаговати док је прикривен другим прозором или overlay-ем</string>
<string name="settings_su_auth_title">Аутентификација корисника</string>
<string name="settings_su_auth_summary">Тражи аутентификацију корисника током захтева супер-корисника</string>
<string name="settings_su_auth_insecure">Ниједан метод аутентификације није подешен на уређају</string>
<string name="settings_customization">Прилагођавање</string>
<string name="setting_add_shortcut_summary">Додај лепу пречицу на почетни екран у случају да се име и иконица не препознају лако након скривања апликације</string>
<string name="settings_doh_title">DNS преко HTTPS-а</string>
<string name="settings_doh_description">Заобилазно решење DNS тровања у неким нацијама</string>
<string name="settings_random_name_title">Насумично име на излазу</string>
<string name="settings_random_name_description">Насумично име излазног фајла слика и tar фајлова ради спречавања детекције</string>
<string name="multiuser_mode">Вишекориснички режим</string>
<string name="settings_owner_only">Само власник уређаја</string>
<string name="settings_owner_manage">Одређено од стране власника</string>
<string name="settings_user_independent">Независно од корисника</string>
<string name="owner_only_summary">Само власник има приступ корену</string>
<string name="owner_manage_summary">Само власник може да приступа корену и да прима захтеве за њега</string>
<string name="user_independent_summary">Сваки корисник има своја правила корена</string>
<string name="mount_namespace_mode">Mount режим namespace-а</string>
<string name="settings_ns_global">Глобални namespace</string>
<string name="settings_ns_requester">Наслеђени namespace</string>
<string name="settings_ns_isolate">Изоловани namespace</string>
<string name="global_summary">Све коренске сесије користе глобални mount namespace</string>
<string name="requester_summary">Коренске сесије ће наследити namespace од подносиоца захтева</string>
<string name="isolate_summary">Свака коренска сесија ће имати свој изоловани namespace</string>
<!--Notifications-->
<string name="update_channel">Ажурирања Magisk-а</string>
<string name="progress_channel">Нотификације о прогресу</string>
<string name="updated_channel">Ажурирање завршено</string>
<string name="download_complete">Преузимање завршено</string>
<string name="download_file_error">Грешка при преузимању фајла</string>
<string name="magisk_update_title">Ажурирање Magisk-а доступно!</string>
<string name="updated_title">Magisk је ажуриран</string>
<string name="updated_text">Кликни да отвориш апликацију</string>
<!--Toasts, Dialogs-->
<string name="yes">Да</string>
<string name="no">Не</string>
<string name="repo_install_title">Инсталирај %1$s %2$s(%3$d)</string>
<string name="download">Преузми</string>
<string name="reboot">Поново покрени</string>
<string name="close">Затвори</string>
<string name="release_notes">Release notes</string>
<string name="flashing">Флешовање…</string>
<string name="running">Покретање…</string>
<string name="done">Завршено!</string>
<string name="done_action">Покретање акције %1$s завршено</string>
<string name="failure">Неуспешно!</string>
<string name="hide_app_title">Скривање Magisk апликације…</string>
<string name="open_link_failed_toast">Није пронађена апликација за отварање линка</string>
<string name="complete_uninstall">Комплетна деинсталација</string>
<string name="restore_img">Поврати слике</string>
<string name="restore_img_msg">Повратак…</string>
<string name="restore_done">Повратак успешан!</string>
<string name="restore_fail">Фабрички бекап не постоји!</string>
<string name="setup_fail">Неуспешна поставка</string>
<string name="env_fix_title">Потребно додатно подешавање</string>
<string name="env_fix_msg">Ваш уређај захтева додатно подешавање да би Magisk радио како треба. Да ли желите наставити и покренути поново?</string>
<string name="env_full_fix_msg">Ваш уређај захтева поновно флешовање да би Magisk радио како треба. Реинсталирајте Magisk кроз апликацију, режим опоравка не може добити тачне информације о уређају.</string>
<string name="setup_msg">Покретање подешавања окружења…</string>
<string name="unsupport_magisk_title">Неподржана верзија Magisk-а</string>
<string name="unsupport_magisk_msg">Ова верзија апликације не подржава Magisk верзије мање од %1$s.\n\nАпликација ће се понашати као да Magisk није инсталиран, молимо ажурирајте Magisk што пре.</string>
<string name="unsupport_general_title">Ненормално стање</string>
<string name="unsupport_system_app_msg">Покретање апликације као системске није подржано. Молимо поставите апликацију да буде корисничка.</string>
<string name="unsupport_other_su_msg">Детектован \"su\" binary који није Magisk-ов. Молимо уклоните конкурентно коренско решење и/или реинсталирајте Magisk.</string>
<string name="unsupport_external_storage_msg">Magisk је инсталиран на екстерно складиште. Молимо померите апл. у интерно складиште.</string>
<string name="unsupport_nonroot_stub_msg">Скривена Magisk апликација не може наставити са радом јер је корен изгубљен. Молимо повратите оригинални APK.</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">Дозволите пермисију за складиште да бисте омогућили ову функционалност</string>
<string name="post_notifications_denied">Дозволите пермисију за нотификације да бисте омогућили ову функционалност</string>
<string name="install_unknown_denied">Дозволите "инсталирање непознатих апликација" да бисте омогућили ову функционалност</string>
<string name="add_shortcut_title">Додај пречицу на почетни екран</string>
<string name="add_shortcut_msg">Након скривања апликације, њено име и иконицу ћете тешко препознати. Желите ли додати лепу пречицу на почетни екран?</string>
<string name="app_not_found">Није пронађена апликација за ову акцију</string>
<string name="reboot_apply_change">Поново покрени да примениш измене</string>
<string name="restore_app_confirmation">Ово ће вратити скривену апликацију на оригиналну. Да ли стварно то желите?</string>
</resources> </resources>

View File

@ -1,234 +0,0 @@
<resources>
<string name="modules">ماڈیولز</string>
<string name="superuser">سپر یوزر</string>
<string name="logs">لاگز</string>
<string name="settings">سیٹنگز</string>
<string name="install">انسٹال کریں</string>
<string name="section_home">ہوم</string>
<string name="section_theme">تھیمز</string>
<string name="denylist">مسترد فہرست</string>
<string name="no_connection">کوئی کنکشن دستیاب نہیں</string>
<string name="app_changelog">ایپ چینج لاگ</string>
<string name="loading">لوڈ ہو رہا ہے…</string>
<string name="update">اپ ڈیٹ</string>
<string name="not_available">دستیاب نہیں</string>
<string name="hide">چھپائیں</string>
<string name="home_package">پیکیج</string>
<string name="home_app_title">ایپ</string>
<string name="home_notice_content">صرف آفیشل گٹ ہب پیج سے Magisk ڈاؤن لوڈ کریں۔ نامعلوم ذرائع سے فائلیں نقصان دہ ہو سکتی ہیں!</string>
<string name="home_support_title">ہماری مدد کریں</string>
<string name="home_follow_title">ہمیں فالو کریں</string>
<string name="home_item_source">ذریعہ</string>
<string name="home_support_content">Magisk ہمیشہ مفت اور اوپن سورس رہے گا۔ تاہم، آپ عطیہ کر کے ہمیں اپنی دیکھ بھال کا اظہار کر سکتے ہیں۔</string>
<string name="home_installed_version">انسٹال شدہ</string>
<string name="home_latest_version">تازہ ترین</string>
<string name="invalid_update_channel">غلط اپ ڈیٹ چینل</string>
<string name="uninstall_magisk_title">Magisk اَن انسٹال کریں</string>
<string name="uninstall_magisk_msg">تمام ماڈیولز غیر فعال/ہٹا دیے جائیں گے!\nروٹ ہٹا دیا جائے گا!\nMagisk کے استعمال کے ذریعے غیر اینکرپٹ کی گئی کوئی بھی اندرونی اسٹوریج دوبارہ اینکرپٹ ہو جائے گی!</string>
<string name="keep_force_encryption">جبری انکرپشن کو محفوظ رکھیں</string>
<string name="keep_dm_verity">AVB 2.0/dm-verity کو محفوظ رکھیں</string>
<string name="recovery_mode">ریکوری موڈ</string>
<string name="install_options_title">آپشنز</string>
<string name="install_method_title">طریقہ</string>
<string name="install_next">اگلا</string>
<string name="install_start">چلو شروع کرتے ہیں</string>
<string name="manager_download_install">ڈاؤن لوڈ اور انسٹال کرنے کے لیے دبائیں</string>
<string name="direct_install">براہ راست انسٹال (تجویز کردہ)</string>
<string name="install_inactive_slot">غیر فعال سلاٹ میں انسٹال کریں (OTA کے بعد)</string>
<string name="install_inactive_slot_msg">ریبوٹ کے بعد آپ کا آلہ زبردستی موجودہ غیر فعال سلاٹ میں بوٹ ہو جائے گا!\nیہ آپشن صرف OTA مکمل ہونے کے بعد استعمال کریں۔\nجاری رکھیں؟</string>
<string name="setup_title">اضافی سیٹ اپ</string>
<string name="select_patch_file">پیچ فائل منتخب اور پیچ کریں</string>
<string name="patch_file_msg">ایک خام امیج (*.img) یا ایک ODIN ٹار فائل (*.tar) یا ایک payload.bin (*.bin) منتخب کریں</string>
<string name="reboot_delay_toast">5 سیکنڈ میں ریبوٹ ہو رہا ہے…</string>
<string name="flash_screen_title">انسٹالیشن</string>
<string name="su_request_title">سپر یوزر کی درخواست</string>
<string name="touch_filtered_warning">چونکہ ایک ایپ سپر یوزر کی درخواست کو دھندلا کر رہی ہے، اس لیے Magisk آپ کے جواب کی تصدیق نہیں کر سکتا</string>
<string name="deny">منع کریں</string>
<string name="prompt">پوچھیں</string>
<string name="grant">اجازت دیں</string>
<string name="su_warning">اپنے آلے تک مکمل رسائی کی اجازت دیں۔\nاگر آپ کو یقین نہیں ہے تو منع کریں!</string>
<string name="forever">ہمیشہ کے لیے</string>
<string name="once">ایک بار</string>
<string name="tenmin">10 منٹ</string>
<string name="twentymin">20 منٹ</string>
<string name="thirtymin">30 منٹ</string>
<string name="sixtymin">60 منٹ</string>
<string name="su_allow_toast">%1$s کو سپر یوزر کے حقوق دیئے گئے</string>
<string name="su_deny_toast">%1$s کو سپر یوزر کے حقوق سے انکار کر دیا گیا</string>
<string name="su_snack_grant">%1$s کے سپر یوزر حقوق دیئے گئے</string>
<string name="su_snack_deny">%1$s کے سپر یوزر حقوق سے انکار کر دیا گیا</string>
<string name="su_snack_notif_on">%1$s کی اطلاعات فعال ہیں</string>
<string name="su_snack_notif_off">%1$s کی اطلاعات غیر فعال ہیں</string>
<string name="su_snack_log_on">%1$s کی لاگنگ فعال ہے</string>
<string name="su_snack_log_off">%1$s کی لاگنگ غیر فعال ہے</string>
<string name="su_revoke_title">منسوخ کریں؟</string>
<string name="su_revoke_msg">%1$s کے سپر یوزر حقوق منسوخ کرنے کی تصدیق کریں</string>
<string name="toast">ٹوسٹ</string>
<string name="none">کوئی نہیں</string>
<string name="superuser_toggle_notification">اطلاعات</string>
<string name="superuser_toggle_revoke">منسوخ کریں</string>
<string name="superuser_policy_none">ابھی تک کسی ایپ نے سپر یوزر کی اجازت نہیں مانگی ہے۔</string>
<string name="log_data_none">آپ لاگ فری ہیں، اپنی روٹ ایپس کو مزید استعمال کرنے کی کوشش کریں</string>
<string name="log_data_magisk_none">Magisk لاگز خالی ہیں، یہ عجیب بات ہے</string>
<string name="menuSaveLog">لاگ محفوظ کریں</string>
<string name="menuClearLog">ابھی لاگ صاف کریں</string>
<string name="logs_cleared">لاگ کامیابی سے صاف ہو گیا</string>
<string name="pid">PID: %1$d</string>
<string name="target_uid">ٹارگٹ UID: %1$d</string>
<string name="target_pid">ماؤنٹ این ایس ٹارگٹ PID: %s</string>
<string name="selinux_context">SELinux سیاق و سباق: %s</string>
<string name="supp_group">اضافی گروپ: %s</string>
<string name="show_system_app">سسٹم ایپس دکھائیں</string>
<string name="show_os_app">OS ایپس دکھائیں</string>
<string name="hide_filter_hint">نام سے فلٹر کریں</string>
<string name="hide_search">تلاش کریں</string>
<string name="no_info_provided">(کوئی معلومات فراہم نہیں کی گئی)</string>
<string name="reboot_userspace">سافٹ ریبوٹ</string>
<string name="reboot_recovery">ریکوری میں ریبوٹ کریں</string>
<string name="reboot_bootloader">بوٹ لوڈر میں ریبوٹ کریں</string>
<string name="reboot_download">ڈاؤن لوڈ میں ریبوٹ کریں</string>
<string name="reboot_edl">EDL میں ریبوٹ کریں</string>
<string name="reboot_safe_mode">محفوظ موڈ</string>
<string name="module_version_author">%2$s کی جانب سے %1$s</string>
<string name="module_state_remove">ہٹائیں</string>
<string name="module_state_restore">بحال کریں</string>
<string name="module_action_install_external">اسٹوریج سے انسٹال کریں</string>
<string name="update_available">اپ ڈیٹ دستیاب ہے</string>
<string name="suspend_text_riru">ماڈیول معطل کر دیا گیا کیونکہ %1$s فعال ہے</string>
<string name="suspend_text_zygisk">ماڈیول معطل کر دیا گیا کیونکہ %1$s فعال نہیں ہے</string>
<string name="zygisk_module_unloaded">غیر مطابقت کی وجہ سے Zygisk ماڈیول لوڈ نہیں ہوا</string>
<string name="module_empty">کوئی ماڈیول انسٹال نہیں ہے</string>
<string name="confirm_install">ماڈیول %1$s انسٹال کریں؟</string>
<string name="confirm_install_title">انسٹالیشن کی تصدیق</string>
<string name="settings_dark_mode_title">تھیم موڈ</string>
<string name="settings_dark_mode_message">وہ موڈ منتخب کریں جو آپ کے انداز کے مطابق ہو!</string>
<string name="settings_dark_mode_light">ہمیشہ لائٹ</string>
<string name="settings_dark_mode_system">سسٹم کی پیروی کریں</string>
<string name="settings_dark_mode_dark">ہمیشہ ڈارک</string>
<string name="settings_download_path_title">ڈاؤن لوڈ کا راستہ</string>
<string name="settings_download_path_message">فائلیں %1$s میں محفوظ کی جائیں گی</string>
<string name="settings_hide_app_title">Magisk ایپ کو چھپائیں</string>
<string name="settings_hide_app_summary">ایک رینڈم پیکیج ID اور کسٹم ایپ لیبل کے ساتھ ایک پراکسی ایپ انسٹال کریں</string>
<string name="settings_restore_app_title">Magisk ایپ کو بحال کریں</string>
<string name="settings_restore_app_summary">ایپ کو ان ہائیڈ کریں اور اصل APK کو بحال کریں</string>
<string name="language">زبان</string>
<string name="system_default">(سسٹم ڈیفالٹ)</string>
<string name="settings_check_update_title">اپ ڈیٹس چیک کریں</string>
<string name="settings_check_update_summary">وقفے وقفے سے پس منظر میں اپ ڈیٹس چیک کریں</string>
<string name="settings_update_channel_title">اپ ڈیٹ چینل</string>
<string name="settings_update_stable">مستحکم</string>
<string name="settings_update_beta">بیٹا</string>
<string name="settings_update_custom">کسٹم</string>
<string name="settings_update_custom_msg">ایک کسٹم چینل URL درج کریں</string>
<string name="settings_zygisk_summary">zygote ڈیمن میں Magisk کے کچھ حصے چلائیں</string>
<string name="settings_denylist_title">مسترد فہرست نافذ کریں</string>
<string name="settings_denylist_summary">مسترد فہرست میں موجود عملوں میں Magisk کی تمام ترامیم کو کالعدم قرار دیا جائے گا</string>
<string name="settings_denylist_config_title">مسترد فہرست کی تشکیل کریں</string>
<string name="settings_denylist_config_summary">مسترد فہرست میں شامل کرنے کے لیے عمل منتخب کریں</string>
<string name="settings_hosts_title">سسٹم لیس ہوسٹس</string>
<string name="settings_hosts_summary">ایڈ بلاکنگ ایپس کے لیے سسٹم لیس ہوسٹس سپورٹ</string>
<string name="settings_hosts_toast">سسٹم لیس ہوسٹس ماڈیول شامل کیا گیا</string>
<string name="settings_app_name_hint">نیا نام</string>
<string name="settings_app_name_helper">ایپ کو اس نام سے دوبارہ پیک کیا جائے گا</string>
<string name="settings_app_name_error">غلط فارمیٹ</string>
<string name="settings_su_app_adb">ایپس اور ADB</string>
<string name="settings_su_app">صرف ایپس</string>
<string name="settings_su_adb">صرف ADB</string>
<string name="settings_su_disable">غیر فعال</string>
<string name="settings_su_request_10">10 سیکنڈ</string>
<string name="settings_su_request_15">15 سیکنڈ</string>
<string name="settings_su_request_20">20 سیکنڈ</string>
<string name="settings_su_request_30">30 سیکنڈ</string>
<string name="settings_su_request_45">45 سیکنڈ</string>
<string name="settings_su_request_60">60 سیکنڈ</string>
<string name="superuser_access">سپر یوزر تک رسائی</string>
<string name="auto_response">خودکار جواب</string>
<string name="request_timeout">درخواست کا وقت ختم</string>
<string name="superuser_notification">سپر یوزر اطلاع</string>
<string name="settings_su_reauth_title">اپ گریڈ کے بعد دوبارہ تصدیق کریں</string>
<string name="settings_su_reauth_summary">ایپس کو اپ گریڈ کرنے کے بعد دوبارہ سپر یوزر کی اجازتیں مانگیں</string>
<string name="settings_su_tapjack_title">ٹیپ جیکنگ سے تحفظ</string>
<string name="settings_su_tapjack_summary">سپر یوزر پرامپٹ ڈائیلاگ کسی دوسری ونڈو یا اوورلے سے دھندلا ہونے کے دوران ان پٹ کا جواب نہیں دے گا</string>
<string name="settings_su_auth_title">صارف کی تصدیق</string>
<string name="settings_su_auth_summary">سپر یوزر کی درخواستوں کے دوران صارف کی تصدیق طلب کریں</string>
<string name="settings_su_auth_insecure">آلے پر کوئی تصدیقی طریقہ کار تشکیل نہیں دیا گیا ہے</string>
<string name="settings_customization">حسب ضرورت سازی</string>
<string name="setting_add_shortcut_summary">اگر ایپ کو چھپانے کے بعد اس کا نام اور آئیکن پہچاننا مشکل ہو جائے تو ہوم اسکرین پر ایک خوبصورت شارٹ کٹ شامل کریں</string>
<string name="settings_doh_title">HTTPS پر DNS</string>
<string name="settings_doh_description">بعض ممالک میں DNS زہر آلودگی کا حل</string>
<string name="settings_random_name_title">بے ترتیب آؤٹ پٹ نام</string>
<string name="settings_random_name_description">پتہ لگانے سے بچنے کے لیے پیچ شدہ امیجز اور ٹار فائلوں کے آؤٹ پٹ فائل کے نام کو بے ترتیب بنائیں</string>
<string name="multiuser_mode">ملٹی یوزر موڈ</string>
<string name="settings_owner_only">صرف آلہ کا مالک</string>
<string name="settings_owner_manage">آلہ کے مالک کے زیر انتظام</string>
<string name="settings_user_independent">صارف سے آزاد</string>
<string name="owner_only_summary">صرف مالک کو روٹ تک رسائی حاصل ہے</string>
<string name="owner_manage_summary">صرف مالک روٹ تک رسائی کا انتظام کر سکتا ہے اور درخواست کے اشارے وصول کر سکتا ہے</string>
<string name="user_independent_summary">ہر صارف کے اپنے علیحدہ روٹ کے قواعد ہیں</string>
<string name="mount_namespace_mode">ماؤنٹ نیم اسپیس موڈ</string>
<string name="settings_ns_global">عالمی نیم اسپیس</string>
<string name="settings_ns_requester">نیم اسپیس وراثت میں حاصل کریں</string>
<string name="settings_ns_isolate">آئسولیٹڈ نیم اسپیس</string>
<string name="isolate_summary">ہر روٹ سیشن کی اپنی آئسولیٹڈ نیم اسپیس ہوگی</string>
<string name="update_channel">Magisk اپ ڈیٹس</string>
<string name="progress_channel">پیش رفت کی اطلاعات</string>
<string name="updated_channel">اپ ڈیٹ مکمل</string>
<string name="download_complete">ڈاؤن لوڈ مکمل</string>
<string name="download_file_error">فائل ڈاؤن لوڈ کرنے میں خرابی</string>
<string name="magisk_update_title">Magisk اپ ڈیٹ دستیاب ہے!</string>
<string name="updated_title">Magisk اپ ڈیٹ ہو گیا</string>
<string name="updated_text">ایپ کھولنے کے لیے ٹیپ کریں</string>
<string name="yes">ہاں</string>
<string name="no">نہیں</string>
<string name="repo_install_title">%1$s %2$s (%3$d) انسٹال کریں</string>
<string name="download">ڈاؤن لوڈ</string>
<string name="reboot">ریبوٹ</string>
<string name="close">بند کریں</string>
<string name="release_notes">ریلیز نوٹ</string>
<string name="flashing">فلیش ہو رہا ہے…</string>
<string name="done">ہو گیا!</string>
<string name="failure">ناکام!</string>
<string name="hide_app_title">Magisk ایپ کو چھپایا جا رہا ہے…</string>
<string name="open_link_failed_toast">لنک کھولنے کے لیے کوئی ایپ نہیں ملی</string>
<string name="complete_uninstall">مکمل ان انسٹال</string>
<string name="restore_img">تصاویر بحال کریں</string>
<string name="restore_img_msg">بحال ہو رہا ہے…</string>
<string name="restore_done">بحالی مکمل ہو گئی!</string>
<string name="restore_fail">اسٹاک بیک اپ موجود نہیں ہے!</string>
<string name="setup_fail">سیٹ اپ ناکام ہو گیا</string>
<string name="env_full_fix_msg">Magisk کو صحیح طریقے سے کام کرنے کے لیے آپ کے آلے کو دوبارہ فلیش کرنے کی ضرورت ہے۔ براہ کرم ایپ کے اندر Magisk کو دوبارہ انسٹال کریں، ریکوری موڈ درست ڈیوائس کی معلومات حاصل نہیں کر سکتا۔</string>
<string name="env_fix_title">اضافی سیٹ اپ درکار ہے</string>
<string name="env_fix_msg">Magisk کو صحیح طریقے سے کام کرنے کے لیے آپ کے آلے کو اضافی سیٹ اپ کی ضرورت ہے۔ کیا آپ جاری رکھنا چاہتے ہیں اور ریبوٹ کرنا چاہتے ہیں؟</string>
<string name="setup_msg">ماحول کا سیٹ اپ چل رہا ہے…</string>
<string name="unsupport_magisk_title">Magisk کا غیر تعاون یافتہ ورژن</string>
<string name="unsupport_magisk_msg">ایپ کا یہ ورژن %1$s سے کم Magisk ورژنز کو سپورٹ نہیں کرتا ہے۔\n\nایپ ایسے برتاؤ کرے گی جیسے کوئی Magisk انسٹال نہیں ہے، براہ کرم جلد از جلد Magisk کو اپ گریڈ کریں۔</string>
<string name="unsupport_general_title">غیر معمولی حالت</string>
<string name="unsupport_system_app_msg">سسٹم ایپ کے طور پر اس ایپ کو چلانا تعاون یافتہ نہیں ہے۔ براہ کرم ایپ کو صارف ایپ میں واپس کریں۔</string>
<string name="unsupport_other_su_msg">Magisk کے علاوہ کسی اور "su" بائنری کا پتہ چلا ہے۔ براہ کرم کسی بھی مسابقتی روٹ حل کو ہٹا دیں اور/یا Magisk کو دوبارہ انسٹال کریں۔</string>
<string name="unsupport_external_storage_msg">Magisk بیرونی اسٹوریج پر انسٹال ہے۔ براہ کرم ایپ کو اندرونی اسٹوریج میں منتقل کریں۔</string>
<string name="unsupport_nonroot_stub_msg">چھپی ہوئی Magisk ایپ کام جاری نہیں رکھ سکتی کیونکہ روٹ ختم ہو گیا ہے۔ براہ کرم اصل APK کو بحال کریں۔</string>
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
<string name="external_rw_permission_denied">اس فعالیت کو فعال کرنے کے لیے اسٹوریج کی اجازت دیں</string>
<string name="post_notifications_denied">اس فعالیت کو فعال کرنے کے لیے اطلاعات کی اجازت دیں</string>
<string name="install_unknown_denied">اس فعالیت کو فعال کرنے کے لیے "نامعلوم ایپس انسٹال کریں" کی اجازت دیں</string>
<string name="add_shortcut_title">ہوم اسکرین پر شارٹ کٹ شامل کریں</string>
<string name="add_shortcut_msg">اس ایپ کو چھپانے کے بعد، اس کا نام اور آئیکن پہچاننا مشکل ہو سکتا ہے۔ کیا آپ ہوم اسکرین پر ایک خوبصورت شارٹ کٹ شامل کرنا چاہتے ہیں؟</string>
<string name="app_not_found">اس عمل کو سنبھالنے کے لیے کوئی ایپ نہیں ملی</string>
<string name="reboot_apply_change">تبدیلیاں لاگو کرنے کے لیے ریبوٹ کریں</string>
<string name="restore_app_confirmation">یہ چھپی ہوئی ایپ کو اصل ایپ میں بحال کردے گا۔ کیا آپ واقعی ایسا کرنا چاہتے ہیں؟</string>
</resources>

View File

@ -25,7 +25,6 @@
<application <application
android:allowBackup="false" android:allowBackup="false"
android:enableOnBackInvokedCallback="false"
android:label="Magisk" android:label="Magisk"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:supportsRtl="true" android:supportsRtl="true"

View File

@ -7,12 +7,12 @@ public class ProviderInstaller {
private static final String GMS_PACKAGE_NAME = "com.google.android.gms"; private static final String GMS_PACKAGE_NAME = "com.google.android.gms";
public static void install(Context context) { public static boolean install(Context context) {
try { try {
// Check if gms is a system app // Check if gms is a system app
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(GMS_PACKAGE_NAME, 0); ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(GMS_PACKAGE_NAME, 0);
if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
return; return false;
} }
// Try installing new SSL provider from Google Play Service // Try installing new SSL provider from Google Play Service
@ -22,7 +22,9 @@ public class ProviderInstaller {
.loadClass("com.google.android.gms.common.security.ProviderInstallerImpl") .loadClass("com.google.android.gms.common.security.ProviderInstallerImpl")
.getMethod("insertProvider", Context.class) .getMethod("insertProvider", Context.class)
.invoke(null, gms); .invoke(null, gms);
} catch (Exception ignored) { } catch (Exception e) {
return false;
} }
return true;
} }
} }

View File

@ -12,7 +12,7 @@ import dalvik.system.BaseDexClassLoader;
public class DynamicClassLoader extends BaseDexClassLoader { public class DynamicClassLoader extends BaseDexClassLoader {
public DynamicClassLoader(File apk) { public DynamicClassLoader(File apk) {
this(apk, DynamicClassLoader.class.getClassLoader()); this(apk, getSystemClassLoader());
} }
public DynamicClassLoader(File apk, ClassLoader parent) { public DynamicClassLoader(File apk, ClassLoader parent) {

View File

@ -13,28 +13,24 @@ android {
namespace = "com.topjohnwu.magisk" namespace = "com.topjohnwu.magisk"
val canary = !Config.version.contains(".") val canary = !Config.version.contains(".")
val base = "https://github.com/topjohnwu/Magisk/releases/download/"
val url = base + "v${Config.version}/Magisk-v${Config.version}.apk" val url = if (canary) null
val canaryUrl = base + "canary-${Config.versionCode}/" else "https://github.com/topjohnwu/Magisk/releases/download/v${Config.version}/Magisk-v${Config.version}.apk"
defaultConfig { defaultConfig {
applicationId = "com.topjohnwu.magisk" applicationId = "com.topjohnwu.magisk"
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
buildConfigField("String", "APK_URL", "\"$url\"") buildConfigField("String", "APK_URL", url?.let { "\"$it\"" } ?: "null" )
buildConfigField("int", "STUB_VERSION", Config.stubVersion) buildConfigField("int", "STUB_VERSION", Config.stubVersion)
} }
buildTypes { buildTypes {
release { release {
if (canary) buildConfigField("String", "APK_URL", "\"${canaryUrl}app-release.apk\"")
proguardFiles("proguard-rules.pro") proguardFiles("proguard-rules.pro")
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = false isShrinkResources = false
} }
debug {
if (canary) buildConfigField("String", "APK_URL", "\"${canaryUrl}app-debug.apk\"")
}
} }
buildFeatures { buildFeatures {
@ -45,5 +41,5 @@ android {
setupStubApk() setupStubApk()
dependencies { dependencies {
implementation(project(":shared")) implementation(project(":app:shared"))
} }

View File

@ -27,6 +27,8 @@ import com.topjohnwu.magisk.net.Networking;
import com.topjohnwu.magisk.net.Request; import com.topjohnwu.magisk.net.Request;
import com.topjohnwu.magisk.utils.APKInstall; import com.topjohnwu.magisk.utils.APKInstall;
import org.json.JSONException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -46,8 +48,13 @@ import javax.crypto.spec.SecretKeySpec;
public class DownloadActivity extends Activity { public class DownloadActivity extends Activity {
private static final String APP_NAME = "Magisk"; private static final String APP_NAME = "Magisk";
private static final String JSON_URL = BuildConfig.DEBUG ?
"https://topjohnwu.github.io/magisk-files/debug.json" :
"https://topjohnwu.github.io/magisk-files/canary.json";
private String apkLink = BuildConfig.APK_URL;
private Context themed; private Context themed;
private ProgressDialog dialog;
private boolean dynLoad; private boolean dynLoad;
@Override @Override
@ -68,7 +75,11 @@ public class DownloadActivity extends Activity {
ProviderInstaller.install(this); ProviderInstaller.install(this);
if (Networking.checkNetworkStatus(this)) { if (Networking.checkNetworkStatus(this)) {
showDialog(); if (BuildConfig.APK_URL == null) {
fetchCanary();
} else {
showDialog();
}
} else { } else {
new AlertDialog.Builder(themed) new AlertDialog.Builder(themed)
.setCancelable(false) .setCancelable(false)
@ -104,10 +115,23 @@ public class DownloadActivity extends Activity {
.show(); .show();
} }
private void fetchCanary() {
dialog = ProgressDialog.show(themed, "", "", true);
request(JSON_URL).getAsJSONObject(json -> {
dialog.dismiss();
try {
apkLink = json.getJSONObject("magisk").getString("link");
showDialog();
} catch (JSONException e) {
error(e);
}
});
}
private void dlAPK() { private void dlAPK() {
ProgressDialog.show(themed, getString(dling), getString(dling) + " " + APP_NAME, true); dialog = ProgressDialog.show(themed, getString(dling), getString(dling) + " " + APP_NAME, true);
// Download and upgrade the app // Download and upgrade the app
var request = request(BuildConfig.APK_URL).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR); var request = request(apkLink).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
if (dynLoad) { if (dynLoad) {
request.getAsFile(StubApk.current(this), file -> StubApk.restartProcess(this)); request.getAsFile(StubApk.current(this), file -> StubApk.restartProcess(this));
} else { } else {

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="upgrade_msg">عليك بتحديث ماجيسك لإكمال تهيئة التطبيق. هل تريد التنزيل والتثبيت؟</string> <string name="upgrade_msg">عليك الترقية Magisk لإكمال تهيئة التطبيق. هل تريد التنزيل والتثبيت؟</string>
<string name="no_internet_msg">يرجى الإتصال بالإنترنت! تحديث ماجيسك مطلوب.</string> <string name="no_internet_msg">يرجى اللإتصال بالإنترنت! ترقية Magisk مطلوبة.</string>
<string name="dling">جارٍ التنزيل</string> <string name="dling">جارٍ التنزيل</string>
<string name="relaunch_app">يرجى إعادة تشغيل التطبيق يدوياً</string> <string name="relaunch_app">يرجى إعادة تشغيل التطبيق يدوياً</string>
</resources> </resources>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Author: Radoš Milićev (https://github.com/rammba)-->
<resources>
<string name="upgrade_msg">Ažurirajte Magisk da biste završili postavljanje. Preuzmi i instaliraj?</string>
<string name="no_internet_msg">Molimo povežite se na internet! Neophodno je ažuriranje Magisk-a.</string>
<string name="dling">Preuzimanje</string>
<string name="relaunch_app">Molimo pokrenite aplikaciju ponovo</string>
</resources>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="upgrade_msg">ماجیسکەکەت بەرزبکەوە بۆ وەشانی تەواوەتی، دەتەوێت دایبگریت و ڕێکیبخەیت؟</string>
<string name="no_internet_msg">تکایە پەیوەست ببە بە ئینتەرنێتەوە، پێویستە ماجیسکەکەت ڕێک بخەیت.</string>
<string name="dling">داگرتن</string>
<string name="relaunch_app">تکایە دووبارە ئەپەکە بکەوە</string>
</resources>

View File

@ -1,8 +1 @@
<?xml version="1.0" encoding="utf-8"?> <resources></resources>
<!--Author: Radoš Milićev (https://github.com/rammba)-->
<resources>
<string name="upgrade_msg">Ажурирајте Magisk да бисте завршили постављање. Преузми и инсталирај?</string>
<string name="no_internet_msg">Молимо повежите се на интернет! Неопходно је ажурирање Magisk-а.</string>
<string name="dling">Преузимање</string>
<string name="relaunch_app">Молимо покрените апликацију поново</string>
</resources>

1
app/test/.gitignore vendored
View File

@ -1 +0,0 @@
/build

View File

@ -1,30 +0,0 @@
plugins {
id("com.android.application")
kotlin("android")
}
android {
namespace = "com.topjohnwu.magisk.test"
defaultConfig {
applicationId = "com.topjohnwu.magisk.test"
versionCode = 1
versionName = "1.0"
proguardFile("proguard-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = true
}
}
}
setupTestApk()
dependencies {
implementation(libs.test.runner)
implementation(libs.test.rules)
implementation(libs.test.junit)
implementation(libs.test.uiautomator)
}

View File

@ -1,13 +0,0 @@
# Keep all test dependencies
-keep class org.junit.** { *; }
-keep class androidx.test.** { *; }
# Make sure the classloader constructor is kept
-keepclassmembers class com.topjohnwu.magisk.test.TestClassLoader { <init>(); }
# Repackage dependencies
-repackageclasses 'deps'
-allowaccessmodification
# Keep attributes for stacktrace
-keepattributes *

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<queries tools:node="removeAll" />
<application tools:node="replace">
<uses-library android:name="android.test.runner" />
</application>
<instrumentation
android:name="com.topjohnwu.magisk.test.AppTestRunner"
android:targetPackage="com.topjohnwu.magisk" />
<instrumentation
android:name="com.topjohnwu.magisk.test.TestRunner"
android:targetPackage="com.topjohnwu.magisk.test" />
</manifest>

View File

@ -1,88 +0,0 @@
package com.topjohnwu.magisk.test
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.ParcelFileDescriptor.AutoCloseInputStream
import androidx.annotation.Keep
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@Keep
@RunWith(AndroidJUnit4::class)
class AppMigrationTest {
companion object {
private const val APP_PKG = "com.topjohnwu.magisk"
private const val STUB_PKG = "repackaged.$APP_PKG"
private const val RECEIVER_TIMEOUT = 20L
}
private val instrumentation get() = InstrumentationRegistry.getInstrumentation()
private val context get() = instrumentation.context
private val uiAutomation get() = instrumentation.uiAutomation
private val registeredReceivers = mutableListOf<BroadcastReceiver>()
class PackageRemoveMonitor(
context: Context,
private val packageName: String
) : BroadcastReceiver() {
val latch = CountDownLatch(1)
init {
val filter = IntentFilter(Intent.ACTION_PACKAGE_REMOVED)
filter.addDataScheme("package")
context.registerReceiver(this, filter)
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_PACKAGE_REMOVED)
return
val data = intent.data ?: return
val pkg = data.schemeSpecificPart
if (pkg == packageName) latch.countDown()
}
}
@After
fun tearDown() {
registeredReceivers.forEach(context::unregisterReceiver)
}
private fun testAppMigration(pkg: String, method: String) {
val receiver = PackageRemoveMonitor(context, pkg)
registeredReceivers.add(receiver)
// Trigger the test to run migration
val pfd = uiAutomation.executeShellCommand(
"am instrument -w --user 0 -e class .Environment#$method " +
"$pkg.test/${AppTestRunner::class.java.name}"
)
val output = AutoCloseInputStream(pfd).reader().use { it.readText() }
assertTrue("$method failed, inst out: $output", output.contains("OK ("))
// Wait for migration to complete
assertTrue(
"$pkg uninstallation failed",
receiver.latch.await(RECEIVER_TIMEOUT, TimeUnit.SECONDS)
)
}
@Test
fun testAppHide() {
testAppMigration(APP_PKG, "setupAppHide")
}
@Test
fun testAppRestore() {
testAppMigration(STUB_PKG, "setupAppRestore")
}
}

View File

@ -1,35 +0,0 @@
package com.topjohnwu.magisk.test
import android.os.Bundle
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnitRunner
open class TestRunner : AndroidJUnitRunner() {
override fun onCreate(arguments: Bundle) {
// Support short-hand ".ClassName"
arguments.getString("class")?.let {
val classArg = it.split(",").joinToString(separator = ",") { clz ->
if (clz.startsWith(".")) {
"com.topjohnwu.magisk.test$clz"
} else {
clz
}
}
arguments.putString("class", classArg)
}
super.onCreate(arguments)
}
}
class AppTestRunner : TestRunner() {
override fun onCreate(arguments: Bundle) {
// Force using the target context's classloader to run tests
arguments.putString("classLoader", TestClassLoader::class.java.name)
super.onCreate(arguments)
}
}
private val targetClassLoader inline get() =
InstrumentationRegistry.getInstrumentation().targetContext.classLoader
class TestClassLoader : ClassLoader(targetClassLoader)

7
build.gradle.kts Normal file
View File

@ -0,0 +1,7 @@
plugins {
id("MagiskPlugin")
}
tasks.register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
}

376
build.py
View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import copy
import glob import glob
import lzma
import multiprocessing import multiprocessing
import os import os
import platform import platform
@ -56,6 +56,14 @@ if is_windows:
if not sys.version_info >= (3, 8): if not sys.version_info >= (3, 8):
error("Requires Python 3.8+") error("Requires Python 3.8+")
try:
sdk_path = Path(os.environ["ANDROID_HOME"])
except KeyError:
try:
sdk_path = Path(os.environ["ANDROID_SDK_ROOT"])
except KeyError:
error("Please set Android SDK path to environment variable ANDROID_HOME")
cpu_count = multiprocessing.cpu_count() cpu_count = multiprocessing.cpu_count()
os_name = platform.system().lower() os_name = platform.system().lower()
@ -67,11 +75,20 @@ support_abis = {
"x86_64": "x86_64-linux-android", "x86_64": "x86_64-linux-android",
"riscv64": "riscv64-linux-android", "riscv64": "riscv64-linux-android",
} }
default_archs = {"armeabi-v7a", "x86", "arm64-v8a", "x86_64"}
default_targets = {"magisk", "magiskinit", "magiskboot", "magiskpolicy"} default_targets = {"magisk", "magiskinit", "magiskboot", "magiskpolicy"}
support_targets = default_targets | {"resetprop"} support_targets = default_targets | {"resetprop"}
rust_targets = {"magisk", "magiskinit", "magiskboot", "magiskpolicy"} rust_targets = {"magisk", "magiskinit", "magiskboot", "magiskpolicy"}
ondk_version = "r29.1"
# Common paths
ndk_root = sdk_path / "ndk"
ndk_path = ndk_root / "magisk"
ndk_build = ndk_path / "ndk-build"
rust_bin = ndk_path / "toolchains" / "rust" / "bin"
llvm_bin = ndk_path / "toolchains" / "llvm" / "prebuilt" / f"{os_name}-x86_64" / "bin"
cargo = rust_bin / "cargo"
gradlew = Path.cwd() / "gradlew"
adb_path = sdk_path / "platform-tools" / "adb"
native_gen_path = Path("native", "out", "generated").resolve()
# Global vars # Global vars
config = {} config = {}
@ -144,31 +161,37 @@ def cmd_out(cmds: list):
) )
def xz(data):
return lzma.compress(data, preset=9, check=lzma.CHECK_NONE)
############### ###############
# Build Native # Build Native
############### ###############
def clean_elf(): def clean_elf():
cargo_toml = Path("tools", "elf-cleaner", "Cargo.toml") if is_windows:
cmds = ["run", "--release", "--manifest-path", cargo_toml] elf_cleaner = Path("tools", "elf-cleaner.exe")
if args.verbose == 0: else:
cmds.append("-q") elf_cleaner = Path("native", "out", "elf-cleaner")
elif args.verbose > 1: if not elf_cleaner.exists():
cmds.append("--verbose") execv(
cmds.append("--") [
"gcc",
'-DPACKAGE_NAME="termux-elf-cleaner"',
'-DPACKAGE_VERSION="2.1.1"',
'-DCOPYRIGHT="Copyright (C) 2022 Termux."',
"tools/termux-elf-cleaner/elf-cleaner.cpp",
"tools/termux-elf-cleaner/arghandling.c",
"-o",
elf_cleaner,
]
)
cmds = [elf_cleaner, "--api-level", "23"]
cmds.extend(glob.glob("native/out/*/magisk")) cmds.extend(glob.glob("native/out/*/magisk"))
cmds.extend(glob.glob("native/out/*/magiskpolicy")) cmds.extend(glob.glob("native/out/*/magiskpolicy"))
run_cargo(cmds) execv(cmds)
def collect_ndk_build():
for arch in build_abis.keys():
arch_dir = Path("native", "libs", arch)
out_dir = Path("native", "out", arch)
for source in arch_dir.iterdir():
target = out_dir / source.name
mv(source, target)
def run_ndk_build(cmds: list): def run_ndk_build(cmds: list):
@ -186,8 +209,17 @@ def run_ndk_build(cmds: list):
error("Build binary failed!") error("Build binary failed!")
os.chdir("..") os.chdir("..")
for arch in build_abis.keys():
arch_dir = Path("native", "libs", arch)
out_dir = Path("native", "out", arch)
for source in arch_dir.iterdir():
target = out_dir / source.name
mv(source, target)
def build_cpp_src(targets: set): def build_cpp_src(targets: set):
dump_flag_header()
cmds = [] cmds = []
clean = False clean = False
@ -207,7 +239,6 @@ def build_cpp_src(targets: set):
if cmds: if cmds:
run_ndk_build(cmds) run_ndk_build(cmds)
collect_ndk_build()
cmds.clear() cmds.clear()
@ -220,18 +251,17 @@ def build_cpp_src(targets: set):
if cmds: if cmds:
cmds.append("B_CRT0=1") cmds.append("B_CRT0=1")
run_ndk_build(cmds) run_ndk_build(cmds)
collect_ndk_build()
if clean: if clean:
clean_elf() clean_elf()
def run_cargo(cmds): def run_cargo(cmds):
ensure_paths()
env = os.environ.copy() env = os.environ.copy()
env["RUSTUP_TOOLCHAIN"] = str(rust_sysroot) env["PATH"] = f'{rust_bin}{os.pathsep}{env["PATH"]}'
env["CARGO_BUILD_RUSTC"] = str(rust_bin / f"rustc{EXE_EXT}")
env["CARGO_BUILD_RUSTFLAGS"] = f"-Z threads={min(8, cpu_count)}" env["CARGO_BUILD_RUSTFLAGS"] = f"-Z threads={min(8, cpu_count)}"
return execv(["cargo", *cmds], env) return execv([cargo, *cmds], env)
def build_rust_src(targets: set): def build_rust_src(targets: set):
@ -304,22 +334,15 @@ def dump_flag_header():
flag_txt += f'#define MAGISK_VER_CODE {config["versionCode"]}\n' flag_txt += f'#define MAGISK_VER_CODE {config["versionCode"]}\n'
flag_txt += f"#define MAGISK_DEBUG {0 if args.release else 1}\n" flag_txt += f"#define MAGISK_DEBUG {0 if args.release else 1}\n"
native_gen_path = Path("native", "out", "generated")
native_gen_path.mkdir(mode=0o755, parents=True, exist_ok=True) native_gen_path.mkdir(mode=0o755, parents=True, exist_ok=True)
write_if_diff(native_gen_path / "flags.h", flag_txt) write_if_diff(Path(native_gen_path, "flags.h"), flag_txt)
rust_flag_txt = f'pub const MAGISK_VERSION: &str = "{config["version"]}";\n'
rust_flag_txt += f'pub const MAGISK_VER_CODE: i32 = {config["versionCode"]};\n'
write_if_diff(native_gen_path / "flags.rs", rust_flag_txt)
def build_native(): def build_native():
ensure_paths()
# Verify NDK install # Verify NDK install
try: try:
with open(Path(ndk_path, "ONDK_VERSION"), "r") as ondk_ver: with open(Path(ndk_path, "ONDK_VERSION"), "r") as ondk_ver:
assert ondk_ver.read().strip(" \t\r\n") == ondk_version assert ondk_ver.read().strip(" \t\r\n") == config["ondkVersion"]
except: except:
error('Unmatched NDK. Please install/upgrade NDK with "build.py ndk"') error('Unmatched NDK. Please install/upgrade NDK with "build.py ndk"')
@ -339,7 +362,6 @@ def build_native():
if ccache := shutil.which("ccache"): if ccache := shutil.which("ccache"):
os.environ["NDK_CCACHE"] = ccache os.environ["NDK_CCACHE"] = ccache
dump_flag_header()
build_rust_src(targets) build_rust_src(targets)
build_cpp_src(targets) build_cpp_src(targets)
@ -375,28 +397,24 @@ def find_jdk():
if no_jdk: if no_jdk:
error( error(
"Please set Android Studio's path to environment variable ANDROID_STUDIO,\n" "Please set Android Studio's path to environment variable ANDROID_STUDIO,\n"
+ "or install JDK 21 and make sure 'javac' is available in PATH" + "or install JDK 17 and make sure 'javac' is available in PATH"
) )
return env return env
def build_apk(module: str): def build_apk(module: str):
ensure_paths()
env = find_jdk() env = find_jdk()
props = args.config.resolve()
os.chdir("app")
build_type = "Release" if args.release else "Debug" build_type = "Release" if args.release else "Debug"
proc = execv( proc = execv(
[ [
gradlew, gradlew,
f"{module}:assemble{build_type}", f"{module}:assemble{build_type}",
f"-PconfigPath={props}", f"-PconfigPath={args.config.resolve()}",
], ],
env=env, env=env,
) )
os.chdir("..")
if proc.returncode != 0: if proc.returncode != 0:
error(f"Build {module} failed!") error(f"Build {module} failed!")
@ -405,23 +423,22 @@ def build_apk(module: str):
paths = module.split(":") paths = module.split(":")
apk = f"{paths[-1]}-{build_type}.apk" apk = f"{paths[-1]}-{build_type}.apk"
source = Path("app", *paths, "build", "outputs", "apk", build_type, apk) source = Path(*paths, "build", "outputs", "apk", build_type, apk)
target = config["outdir"] / apk target = config["outdir"] / apk
mv(source, target) mv(source, target)
return target header(f"Output: {target}")
def build_app(): def build_app():
header("* Building the Magisk app") header("* Building the Magisk app")
apk = build_apk(":apk") build_apk(":app:apk")
build_type = "release" if args.release else "debug" build_type = "release" if args.release else "debug"
# Rename apk-variant.apk to app-variant.apk # Rename apk-variant.apk to app-variant.apk
source = apk source = config["outdir"] / f"apk-{build_type}.apk"
target = apk.parent / apk.name.replace("apk-", "app-") target = config["outdir"] / f"app-{build_type}.apk"
mv(source, target) mv(source, target)
header(f"Output: {target}")
# Stub building is directly integrated into the main app # Stub building is directly integrated into the main app
# build process. Copy the stub APK into output directory. # build process. Copy the stub APK into output directory.
@ -432,23 +449,7 @@ def build_app():
def build_stub(): def build_stub():
header("* Building the stub app") header("* Building the stub app")
apk = build_apk(":stub") build_apk(":app:stub")
header(f"Output: {apk}")
def build_test():
global args
args_bak = copy.copy(args)
# Test APK has to be built as release to prevent classname clash
args.release = True
try:
header("* Building the test app")
source = build_apk(":test")
target = source.parent / "test.apk"
mv(source, target)
header(f"Output: {target}")
finally:
args = args_bak
################ ################
@ -457,7 +458,6 @@ def build_test():
def cleanup(): def cleanup():
ensure_paths()
support_targets = {"native", "cpp", "rust", "app"} support_targets = {"native", "cpp", "rust", "app"}
if args.targets: if args.targets:
targets = set(args.targets) & support_targets targets = set(args.targets) & support_targets
@ -481,21 +481,16 @@ def cleanup():
rm(rs_gen) rm(rs_gen)
if "native" in targets: if "native" in targets:
header("* Cleaning native")
rm_rf(Path("native", "out")) rm_rf(Path("native", "out"))
rm_rf(Path("tools", "elf-cleaner", "target"))
if "app" in targets: if "app" in targets:
header("* Cleaning app") header("* Cleaning app")
os.chdir("app") execv([gradlew, ":app:clean"], env=find_jdk())
execv([gradlew, ":clean"], env=find_jdk())
os.chdir("..")
def build_all(): def build_all():
build_native() build_native()
build_app() build_app()
build_test()
############ ############
@ -503,46 +498,6 @@ def build_all():
############ ############
def gen_ide():
ensure_paths()
set_archs({args.abi})
# Dump flags for both C++ and Rust code
dump_flag_header()
# Run build.rs to generate Rust/C++ FFI bindings
os.chdir(Path("native", "src"))
run_cargo(["check"])
os.chdir(Path("..", ".."))
# Generate compilation database
rm_rf(Path("native", "compile_commands.json"))
run_ndk_build(
[
"B_MAGISK=1",
"B_INIT=1",
"B_BOOT=1",
"B_POLICY=1",
"B_PRELOAD=1",
"B_PROP=1",
"B_CRT0=1",
"compile_commands.json",
]
)
def clippy_cli():
args.force_out = True
set_archs(default_archs)
os.chdir(Path("native", "src"))
cmds = ["clippy", "--no-deps", "--target"]
for triple in build_abis.values():
run_cargo(cmds + [triple])
run_cargo(cmds + [triple, "--release"])
os.chdir(Path("..", ".."))
def cargo_cli(): def cargo_cli():
args.force_out = True args.force_out = True
if len(args.commands) >= 1 and args.commands[0] == "--": if len(args.commands) >= 1 and args.commands[0] == "--":
@ -553,10 +508,10 @@ def cargo_cli():
def setup_ndk(): def setup_ndk():
ensure_paths() ndk_ver = config["ondkVersion"]
url = f"https://github.com/topjohnwu/ondk/releases/download/{ondk_version}/ondk-{ondk_version}-{os_name}.tar.xz" url = f"https://github.com/topjohnwu/ondk/releases/download/{ndk_ver}/ondk-{ndk_ver}-{os_name}.tar.xz"
ndk_archive = url.split("/")[-1] ndk_archive = url.split("/")[-1]
ondk_path = Path(ndk_root, f"ondk-{ondk_version}") ondk_path = Path(ndk_root, f"ondk-{ndk_ver}")
header(f"* Downloading and extracting {ndk_archive}") header(f"* Downloading and extracting {ndk_archive}")
rm_rf(ondk_path) rm_rf(ondk_path)
@ -571,54 +526,14 @@ def setup_ndk():
mv(ondk_path, ndk_path) mv(ondk_path, ndk_path)
def setup_rustup():
wrapper_dir = Path(args.wrapper_dir)
rm_rf(wrapper_dir)
wrapper_dir.mkdir(mode=0o755, parents=True, exist_ok=True)
if "CARGO_HOME" in os.environ:
cargo_home = Path(os.environ["CARGO_HOME"])
else:
cargo_home = Path.home() / ".cargo"
cargo_bin = cargo_home / "bin"
for src in cargo_bin.iterdir():
tgt = wrapper_dir / src.name
tgt.symlink_to(f"rustup{EXE_EXT}")
# Build rustup-wrapper
wrapper_src = Path("tools", "rustup-wrapper")
cargo_toml = wrapper_src / "Cargo.toml"
cmds = ["build", "--release", f"--manifest-path={cargo_toml}"]
if args.verbose > 1:
cmds.append("--verbose")
run_cargo(cmds)
# Replace rustup with wrapper
wrapper = wrapper_dir / (f"rustup{EXE_EXT}")
wrapper.unlink(missing_ok=True)
cp(wrapper_src / "target" / "release" / (f"rustup-wrapper{EXE_EXT}"), wrapper)
wrapper.chmod(0o755)
##################
# AVD and testing
##################
def push_files(script): def push_files(script):
if args.build:
build_all()
ensure_adb()
abi = cmd_out([adb_path, "shell", "getprop", "ro.product.cpu.abi"]) abi = cmd_out([adb_path, "shell", "getprop", "ro.product.cpu.abi"])
if not abi: if not abi:
error("Cannot detect emulator ABI") error("Cannot detect emulator ABI")
if args.apk: apk = Path(
apk = Path(args.apk) config["outdir"], ("app-release.apk" if args.release else "app-debug.apk")
else: )
apk = Path(
config["outdir"], ("app-release.apk" if args.release else "app-debug.apk")
)
# Extract busybox from APK # Extract busybox from APK
busybox = Path(config["outdir"], "busybox") busybox = Path(config["outdir"], "busybox")
@ -640,33 +555,42 @@ def push_files(script):
def setup_avd(): def setup_avd():
if not args.skip:
build_all()
header("* Setting up emulator") header("* Setting up emulator")
push_files(Path("scripts", "live_setup.sh")) push_files(Path("scripts", "avd_magisk.sh"))
proc = execv([adb_path, "shell", "sh", "/data/local/tmp/live_setup.sh"]) proc = execv([adb_path, "shell", "sh", "/data/local/tmp/avd_magisk.sh"])
if proc.returncode != 0: if proc.returncode != 0:
error("live_setup.sh failed!") error("avd_magisk.sh failed!")
def patch_avd_file(): def patch_avd_file():
if not args.skip:
build_all()
input = Path(args.image) input = Path(args.image)
output = Path(args.output) if args.output:
output = Path(args.output)
else:
output = input.parent / f"{input.name}.magisk"
src_file = f"/data/local/tmp/{input.name}"
out_file = f"{src_file}.magisk"
header(f"* Patching {input.name}") header(f"* Patching {input.name}")
push_files(Path("scripts", "host_patch.sh")) push_files(Path("scripts", "avd_patch.sh"))
proc = execv([adb_path, "push", input, "/data/local/tmp"]) proc = execv([adb_path, "push", input, "/data/local/tmp"])
if proc.returncode != 0: if proc.returncode != 0:
error("adb push failed!") error("adb push failed!")
src_file = f"/data/local/tmp/{input.name}" proc = execv([adb_path, "shell", "sh", "/data/local/tmp/avd_patch.sh", src_file])
out_file = f"{src_file}.magisk"
proc = execv([adb_path, "shell", "sh", "/data/local/tmp/host_patch.sh", src_file])
if proc.returncode != 0: if proc.returncode != 0:
error("host_patch.sh failed!") error("avd_patch.sh failed!")
proc = execv([adb_path, "pull", out_file, output]) proc = execv([adb_path, "pull", out_file, output])
if proc.returncode != 0: if proc.returncode != 0:
@ -675,47 +599,37 @@ def patch_avd_file():
header(f"Output: {output}") header(f"Output: {output}")
########################## def setup_rustup():
# Config, paths, argparse wrapper_dir = Path(args.wrapper_dir)
########################## rm_rf(wrapper_dir)
wrapper_dir.mkdir(mode=0o755, parents=True, exist_ok=True)
if "CARGO_HOME" in os.environ:
cargo_home = Path(os.environ["CARGO_HOME"])
else:
cargo_home = Path.home() / ".cargo"
cargo_bin = cargo_home / "bin"
for src in cargo_bin.iterdir():
tgt = wrapper_dir / src.name
tgt.symlink_to(src)
# Build rustup_wrapper
wrapper_src = Path("tools", "rustup_wrapper")
cargo_toml = wrapper_src / "Cargo.toml"
cmds = ["build", "--release", f"--manifest-path={cargo_toml}"]
if args.verbose > 1:
cmds.append("--verbose")
run_cargo(cmds)
# Replace rustup with wrapper
wrapper = wrapper_dir / (f"rustup{EXE_EXT}")
wrapper.unlink(missing_ok=True)
cp(wrapper_src / "target" / "release" / (f"rustup_wrapper{EXE_EXT}"), wrapper)
wrapper.chmod(0o755)
def ensure_paths(): ##################
global sdk_path, ndk_root, ndk_path, ndk_build, rust_sysroot # Config and args
global llvm_bin, gradlew, adb_path, native_gen_path ##################
# Skip if already initialized
if "sdk_path" in globals():
return
try:
sdk_path = Path(os.environ["ANDROID_HOME"])
except KeyError:
try:
sdk_path = Path(os.environ["ANDROID_SDK_ROOT"])
except KeyError:
error("Please set Android SDK path to environment variable ANDROID_HOME")
ndk_root = sdk_path / "ndk"
ndk_path = ndk_root / "magisk"
ndk_build = ndk_path / "ndk-build"
rust_sysroot = ndk_path / "toolchains" / "rust"
llvm_bin = (
ndk_path / "toolchains" / "llvm" / "prebuilt" / f"{os_name}-x86_64" / "bin"
)
adb_path = sdk_path / "platform-tools" / "adb"
gradlew = Path.cwd() / "app" / "gradlew"
# We allow using several functionality with only ADB
def ensure_adb():
global adb_path
if "adb_path" not in globals():
adb_path = shutil.which("adb")
if not adb_path:
error("Command 'adb' cannot be found in PATH")
else:
adb_path = Path(adb_path)
def parse_props(file): def parse_props(file):
@ -735,12 +649,6 @@ def parse_props(file):
return props return props
def set_archs(archs: set):
triples = map(support_abis.get, archs)
global build_abis
build_abis = dict(zip(archs, triples))
def load_config(): def load_config():
commit_hash = cmd_out(["git", "rev-parse", "--short=8", "HEAD"]) commit_hash = cmd_out(["git", "rev-parse", "--short=8", "HEAD"])
@ -755,9 +663,8 @@ def load_config():
if args.config.exists(): if args.config.exists():
config.update(parse_props(args.config)) config.update(parse_props(args.config))
gradle_props = Path("app", "gradle.properties") if Path("gradle.properties").exists():
if gradle_props.exists(): for key, value in parse_props("gradle.properties").items():
for key, value in parse_props(gradle_props).items():
if key.startswith("magisk."): if key.startswith("magisk."):
config[key[7:]] = value config[key[7:]] = value
@ -773,9 +680,12 @@ def load_config():
abiList = re.split("\\s*,\\s*", config["abiList"]) abiList = re.split("\\s*,\\s*", config["abiList"])
archs = set(abiList) & support_abis.keys() archs = set(abiList) & support_abis.keys()
else: else:
archs = default_archs archs = {"armeabi-v7a", "x86", "arm64-v8a", "x86_64"}
set_archs(archs) triples = map(support_abis.get, archs)
global build_abis
build_abis = dict(zip(archs, triples))
def parse_args(): def parse_args():
@ -809,8 +719,6 @@ def parse_args():
stub_parser = subparsers.add_parser("stub", help="build the stub app") stub_parser = subparsers.add_parser("stub", help="build the stub app")
test_parser = subparsers.add_parser("test", help="build the test app")
clean_parser = subparsers.add_parser("clean", help="cleanup") clean_parser = subparsers.add_parser("clean", help="cleanup")
clean_parser.add_argument( clean_parser.add_argument(
"targets", nargs="*", help="native, cpp, rust, java, or empty to clean all" "targets", nargs="*", help="native, cpp, rust, java, or empty to clean all"
@ -819,19 +727,17 @@ def parse_args():
ndk_parser = subparsers.add_parser("ndk", help="setup Magisk NDK") ndk_parser = subparsers.add_parser("ndk", help="setup Magisk NDK")
emu_parser = subparsers.add_parser("emulator", help="setup AVD for development") emu_parser = subparsers.add_parser("emulator", help="setup AVD for development")
emu_parser.add_argument("apk", help="a Magisk APK to use", nargs="?")
emu_parser.add_argument( emu_parser.add_argument(
"-b", "--build", action="store_true", help="build before patching" "-s", "--skip", action="store_true", help="skip building binaries and the app"
) )
avd_patch_parser = subparsers.add_parser( avd_patch_parser = subparsers.add_parser(
"avd_patch", help="patch AVD ramdisk.img or init_boot.img" "avd_patch", help="patch AVD ramdisk.img or init_boot.img"
) )
avd_patch_parser.add_argument("image", help="path to ramdisk.img or init_boot.img") avd_patch_parser.add_argument("image", help="path to ramdisk.img or init_boot.img")
avd_patch_parser.add_argument("output", help="output file name") avd_patch_parser.add_argument("output", help="optional output file name", nargs="?")
avd_patch_parser.add_argument("--apk", help="a Magisk APK to use")
avd_patch_parser.add_argument( avd_patch_parser.add_argument(
"-b", "--build", action="store_true", help="build before patching" "-s", "--skip", action="store_true", help="skip building binaries and the app"
) )
cargo_parser = subparsers.add_parser( cargo_parser = subparsers.add_parser(
@ -839,26 +745,18 @@ def parse_args():
) )
cargo_parser.add_argument("commands", nargs=argparse.REMAINDER) cargo_parser.add_argument("commands", nargs=argparse.REMAINDER)
clippy_parser = subparsers.add_parser("clippy", help="run clippy on Rust sources")
rustup_parser = subparsers.add_parser("rustup", help="setup rustup wrapper") rustup_parser = subparsers.add_parser("rustup", help="setup rustup wrapper")
rustup_parser.add_argument( rustup_parser.add_argument(
"wrapper_dir", help="path to setup rustup wrapper binaries" "wrapper_dir", help="path to setup rustup wrapper binaries"
) )
gen_parser = subparsers.add_parser("gen", help="generate files for IDE")
gen_parser.add_argument("--abi", default="arm64-v8a", help="target ABI to generate")
# Set callbacks # Set callbacks
all_parser.set_defaults(func=build_all) all_parser.set_defaults(func=build_all)
native_parser.set_defaults(func=build_native) native_parser.set_defaults(func=build_native)
cargo_parser.set_defaults(func=cargo_cli) cargo_parser.set_defaults(func=cargo_cli)
clippy_parser.set_defaults(func=clippy_cli)
rustup_parser.set_defaults(func=setup_rustup) rustup_parser.set_defaults(func=setup_rustup)
gen_parser.set_defaults(func=gen_ide)
app_parser.set_defaults(func=build_app) app_parser.set_defaults(func=build_app)
stub_parser.set_defaults(func=build_stub) stub_parser.set_defaults(func=build_stub)
test_parser.set_defaults(func=build_test)
emu_parser.set_defaults(func=setup_avd) emu_parser.set_defaults(func=setup_avd)
avd_patch_parser.set_defaults(func=patch_avd_file) avd_patch_parser.set_defaults(func=patch_avd_file)
clean_parser.set_defaults(func=cleanup) clean_parser.set_defaults(func=cleanup)
@ -871,13 +769,7 @@ def parse_args():
return parser.parse_args() return parser.parse_args()
def main(): args = parse_args()
global args load_config()
args = parse_args() vars(args)["force_out"] = False
load_config() args.func()
vars(args)["force_out"] = False
args.func()
if __name__ == "__main__":
main()

View File

@ -7,10 +7,8 @@ import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes import org.objectweb.asm.Opcodes
import org.objectweb.asm.Opcodes.ASM9 import org.objectweb.asm.Opcodes.ASM9
private const val DESUGAR_CLASS_NAME = "com.topjohnwu.magisk.core.utils.Desugar"
private const val ZIP_ENTRY_CLASS_NAME = "java.util.zip.ZipEntry" private const val ZIP_ENTRY_CLASS_NAME = "java.util.zip.ZipEntry"
private const val ZIP_OUT_STREAM_CLASS_NAME = "org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream" private const val DESUGAR_CLASS_NAME = "com.topjohnwu.magisk.core.utils.Desugar"
private const val ZIP_UTIL_CLASS_NAME = "org/apache/commons/compress/archivers/zip/ZipUtil"
private const val ZIP_ENTRY_GET_TIME_DESC = "()Ljava/nio/file/attribute/FileTime;" private const val ZIP_ENTRY_GET_TIME_DESC = "()Ljava/nio/file/attribute/FileTime;"
private const val DESUGAR_GET_TIME_DESC = private const val DESUGAR_GET_TIME_DESC =
"(Ljava/util/zip/ZipEntry;)Ljava/nio/file/attribute/FileTime;" "(Ljava/util/zip/ZipEntry;)Ljava/nio/file/attribute/FileTime;"
@ -22,29 +20,27 @@ abstract class DesugarClassVisitorFactory : AsmClassVisitorFactory<Instrumentati
classContext: ClassContext, classContext: ClassContext,
nextClassVisitor: ClassVisitor nextClassVisitor: ClassVisitor
): ClassVisitor { ): ClassVisitor {
return if (classContext.currentClassData.className == ZIP_OUT_STREAM_CLASS_NAME) { return DesugarClassVisitor(classContext, nextClassVisitor)
ZipEntryPatcher(classContext, ZipOutputStreamPatcher(nextClassVisitor))
} else {
ZipEntryPatcher(classContext, nextClassVisitor)
}
} }
override fun isInstrumentable(classData: ClassData) = classData.className != DESUGAR_CLASS_NAME override fun isInstrumentable(classData: ClassData) = classData.className != DESUGAR_CLASS_NAME
// Patch ALL references to ZipEntry#getXXXTime class DesugarClassVisitor(private val classContext: ClassContext, cv: ClassVisitor) :
class ZipEntryPatcher( ClassVisitor(ASM9, cv) {
private val classContext: ClassContext,
cv: ClassVisitor
) : ClassVisitor(ASM9, cv) {
override fun visitMethod( override fun visitMethod(
access: Int, access: Int,
name: String?, name: String?,
descriptor: String?, descriptor: String?,
signature: String?, signature: String?,
exceptions: Array<out String>? exceptions: Array<out String>?
) = MethodPatcher(super.visitMethod(access, name, descriptor, signature, exceptions)) ): MethodVisitor {
return DesugarMethodVisitor(
super.visitMethod(access, name, descriptor, signature, exceptions)
)
}
inner class MethodPatcher(mv: MethodVisitor?) : MethodVisitor(ASM9, mv) { inner class DesugarMethodVisitor(mv: MethodVisitor?) :
MethodVisitor(ASM9, mv) {
override fun visitMethodInsn( override fun visitMethodInsn(
opcode: Int, opcode: Int,
owner: String, owner: String,
@ -79,44 +75,4 @@ abstract class DesugarClassVisitorFactory : AsmClassVisitorFactory<Instrumentati
} }
} }
} }
// Patch ZipArchiveOutputStream#copyFromZipInputStream
class ZipOutputStreamPatcher(cv: ClassVisitor) : ClassVisitor(ASM9, cv) {
override fun visitMethod(
access: Int,
name: String,
descriptor: String,
signature: String?,
exceptions: Array<out String?>?
): MethodVisitor? {
return if (name == "copyFromZipInputStream") {
MethodPatcher(super.visitMethod(access, name, descriptor, signature, exceptions))
} else {
super.visitMethod(access, name, descriptor, signature, exceptions)
}
}
class MethodPatcher(mv: MethodVisitor?) : MethodVisitor(ASM9, mv) {
override fun visitMethodInsn(
opcode: Int,
owner: String,
name: String,
descriptor: String?,
isInterface: Boolean
) {
if (owner == ZIP_UTIL_CLASS_NAME && name == "checkRequestedFeatures") {
// Redirect
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
DESUGAR_CLASS_NAME.replace('.', '/'),
name,
descriptor,
false
)
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
}
}
}
}
} }

View File

@ -14,7 +14,7 @@ private val defaultAbis = setOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
object Config { object Config {
operator fun get(key: String): String? { operator fun get(key: String): String? {
val v = props[key] as? String ?: return null val v = props[key] as? String ?: return null
return v.ifBlank { null } return if (v.isBlank()) null else v
} }
fun contains(key: String) = get(key) != null fun contains(key: String) = get(key) != null
@ -28,25 +28,19 @@ object Config {
} }
} }
fun Project.rootFile(path: String): File {
val file = File(path)
return if (file.isAbsolute) file
else File(rootProject.file(".."), path)
}
class MagiskPlugin : Plugin<Project> { class MagiskPlugin : Plugin<Project> {
override fun apply(project: Project) = project.applyPlugin() override fun apply(project: Project) = project.applyPlugin()
private fun Project.applyPlugin() { private fun Project.applyPlugin() {
initRandom(rootProject.file("dict.txt")) initRandom(rootProject.file("app/dict.txt"))
props.clear() props.clear()
rootProject.file("gradle.properties").inputStream().use { props.load(it) } rootProject.file("gradle.properties").inputStream().use { props.load(it) }
val configPath: String? by this val configPath: String? by this
val config = rootFile(configPath ?: "config.prop") val config = configPath?.let { File(it) } ?: rootProject.file("config.prop")
if (config.exists()) if (config.exists())
config.inputStream().use { props.load(it) } config.inputStream().use { props.load(it) }
val repo = FileRepository(rootFile(".git")) val repo = FileRepository(rootProject.file(".git"))
val refId = repo.refDatabase.exactRef("HEAD").objectId val refId = repo.refDatabase.exactRef("HEAD").objectId
commitHash = repo.newObjectReader().abbreviate(refId, 8).name() commitHash = repo.newObjectReader().abbreviate(refId, 8).name()
} }

View File

@ -17,6 +17,7 @@ import org.gradle.api.Action
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.JavaVersion import org.gradle.api.JavaVersion
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property import org.gradle.api.provider.Property
import org.gradle.api.tasks.Copy import org.gradle.api.tasks.Copy
@ -71,10 +72,10 @@ private val Project.androidComponents
fun Project.setupCommon() { fun Project.setupCommon() {
androidBase { androidBase {
compileSdkVersion(36) compileSdkVersion(35)
buildToolsVersion = "36.0.0" buildToolsVersion = "34.0.0"
ndkPath = "$sdkDirectory/ndk/magisk" ndkPath = "$sdkDirectory/ndk/magisk"
ndkVersion = "29.0.13113456" ndkVersion = "27.0.12077973"
defaultConfig { defaultConfig {
minSdk = 23 minSdk = 23
@ -89,7 +90,6 @@ fun Project.setupCommon() {
resources { resources {
excludes += arrayOf( excludes += arrayOf(
"/META-INF/*", "/META-INF/*",
"/META-INF/androidx/**",
"/META-INF/versions/**", "/META-INF/versions/**",
"/org/bouncycastle/**", "/org/bouncycastle/**",
"/org/apache/commons/**", "/org/apache/commons/**",
@ -116,27 +116,6 @@ fun Project.setupCommon() {
} }
} }
private fun Project.downloadFile(url: String, checksum: String): File {
val file = layout.buildDirectory.file(checksum).get().asFile
if (file.exists()) {
val md = MessageDigest.getInstance("SHA-256")
file.inputStream().use { md.update(it.readAllBytes()) }
val hash = HexFormat.of().formatHex(md.digest())
if (hash != checksum) {
file.delete()
}
}
if (!file.exists()) {
file.parentFile.mkdirs()
URI(url).toURL().openStream().use { dl ->
file.outputStream().use {
dl.copyTo(it)
}
}
}
return file
}
const val BUSYBOX_DOWNLOAD_URL = const val BUSYBOX_DOWNLOAD_URL =
"https://github.com/topjohnwu/magisk-files/releases/download/files/busybox-1.36.1.1.zip" "https://github.com/topjohnwu/magisk-files/releases/download/files/busybox-1.36.1.1.zip"
const val BUSYBOX_ZIP_CHECKSUM = const val BUSYBOX_ZIP_CHECKSUM =
@ -151,7 +130,7 @@ fun Project.setupCoreLib() {
into("src/main/jniLibs") into("src/main/jniLibs")
for (abi in abiList) { for (abi in abiList) {
into(abi) { into(abi) {
from(rootFile("native/out/$abi")) { from(rootProject.file("native/out/$abi")) {
include("magiskboot", "magiskinit", "magiskpolicy", "magisk", "libinit-ld.so") include("magiskboot", "magiskinit", "magiskpolicy", "magisk", "libinit-ld.so")
rename { if (it.endsWith(".so")) it else "lib$it.so" } rename { if (it.endsWith(".so")) it else "lib$it.so" }
} }
@ -166,17 +145,34 @@ fun Project.setupCoreLib() {
val downloadBusybox by tasks.registering(Copy::class) { val downloadBusybox by tasks.registering(Copy::class) {
dependsOn(syncLibs) dependsOn(syncLibs)
from(zipTree(downloadFile(BUSYBOX_DOWNLOAD_URL, BUSYBOX_ZIP_CHECKSUM))) val bb = layout.buildDirectory.file(BUSYBOX_ZIP_CHECKSUM).get().asFile
if (bb.exists()) {
val md = MessageDigest.getInstance("SHA-256")
bb.inputStream().use { md.update(it.readAllBytes()) }
val hash = HexFormat.of().formatHex(md.digest())
if (hash != BUSYBOX_ZIP_CHECKSUM) {
bb.delete()
}
}
if (!bb.exists()) {
bb.parentFile.mkdirs()
URI(BUSYBOX_DOWNLOAD_URL).toURL().openStream().use { dl ->
bb.outputStream().use {
dl.copyTo(it)
}
}
}
from(zipTree(bb))
include(abiList.map { "$it/libbusybox.so" }) include(abiList.map { "$it/libbusybox.so" })
into("src/main/jniLibs") into("src/main/jniLibs")
} }
val syncResources by tasks.registering(Sync::class) { val syncResources by tasks.registering(Sync::class) {
into("src/main/resources/META-INF/com/google/android") into("src/main/resources/META-INF/com/google/android")
from(rootFile("scripts/update_binary.sh")) { from(rootProject.file("scripts/update_binary.sh")) {
rename { "update-binary" } rename { "update-binary" }
} }
from(rootFile("scripts/flash_script.sh")) { from(rootProject.file("scripts/flash_script.sh")) {
rename { "updater-script" } rename { "updater-script" }
} }
} }
@ -187,7 +183,7 @@ fun Project.setupCoreLib() {
tasks.getByPath("merge${variantCapped}JniLibFolders").dependsOn(downloadBusybox) tasks.getByPath("merge${variantCapped}JniLibFolders").dependsOn(downloadBusybox)
processJavaResourcesProvider.configure { dependsOn(syncResources) } processJavaResourcesProvider.configure { dependsOn(syncResources) }
val stubTask = tasks.getByPath(":stub:comment$variantCapped") val stubTask = tasks.getByPath(":app:stub:comment$variantCapped")
val stubApk = stubTask.outputs.files.asFileTree.filter { val stubApk = stubTask.outputs.files.asFileTree.filter {
it.name.endsWith(".apk") it.name.endsWith(".apk")
} }
@ -197,14 +193,14 @@ fun Project.setupCoreLib() {
inputs.property("version", Config.version) inputs.property("version", Config.version)
inputs.property("versionCode", Config.versionCode) inputs.property("versionCode", Config.versionCode)
into("src/${this@all.name}/assets") into("src/${this@all.name}/assets")
from(rootFile("scripts")) { from(rootProject.file("scripts")) {
include("util_functions.sh", "boot_patch.sh", "addon.d.sh", include("util_functions.sh", "boot_patch.sh", "addon.d.sh",
"app_functions.sh", "uninstaller.sh", "module_installer.sh") "app_functions.sh", "uninstaller.sh", "module_installer.sh")
} }
from(rootFile("tools/bootctl")) from(rootProject.file("tools/bootctl"))
into("chromeos") { into("chromeos") {
from(rootFile("tools/futility")) from(rootProject.file("tools/futility"))
from(rootFile("tools/keys")) { from(rootProject.file("tools/keys")) {
include("kernel_data_key.vbprivk", "kernel.keyblock") include("kernel_data_key.vbprivk", "kernel.keyblock")
} }
} }
@ -292,9 +288,9 @@ fun Project.setupAppCommon() {
android { android {
signingConfigs { signingConfigs {
Config["keyStore"]?.also { create("config") {
create("config") { Config["keyStore"]?.also {
storeFile = rootFile(it) storeFile = rootProject.file(it)
storePassword = Config["keyStorePass"] storePassword = Config["keyStorePass"]
keyAlias = Config["keyAlias"] keyAlias = Config["keyAlias"]
keyPassword = Config["keyPass"] keyPassword = Config["keyPass"]
@ -303,19 +299,19 @@ fun Project.setupAppCommon() {
} }
defaultConfig { defaultConfig {
targetSdk = 36 targetSdk = 35
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt")
)
} }
buildTypes { buildTypes {
val config = signingConfigs.findByName("config") ?: signingConfigs["debug"] signingConfigs["config"].also {
debug { debug {
signingConfig = config signingConfig = if (it.storeFile?.exists() == true) it
} else signingConfigs["debug"]
release { }
signingConfig = config release {
signingConfig = if (it.storeFile?.exists() == true) it
else signingConfigs["debug"]
}
} }
} }
@ -464,31 +460,3 @@ fun Project.setupStubApk() {
delete.addAll(listOf("src/debug/AndroidManifest.xml", "src/release/AndroidManifest.xml")) delete.addAll(listOf("src/debug/AndroidManifest.xml", "src/release/AndroidManifest.xml"))
} }
} }
const val LSPOSED_DOWNLOAD_URL =
"https://github.com/LSPosed/LSPosed/releases/download/v1.9.2/LSPosed-v1.9.2-7024-zygisk-release.zip"
const val LSPOSED_CHECKSUM =
"0ebc6bcb465d1c4b44b7220ab5f0252e6b4eb7fe43da74650476d2798bb29622"
const val SHAMIKO_DOWNLOAD_URL =
"https://github.com/LSPosed/LSPosed.github.io/releases/download/shamiko-383/Shamiko-v1.2.1-383-release.zip"
const val SHAMIKO_CHECKSUM =
"93754a038c2d8f0e985bad45c7303b96f70a93d8335060e50146f028d3a9b13f"
fun Project.setupTestApk() {
setupAppCommon()
androidApp.applicationVariants.all {
val variantCapped = name.replaceFirstChar { it.uppercase() }
val dlTask by tasks.register("download${variantCapped}Lsposed", Sync::class) {
from(downloadFile(LSPOSED_DOWNLOAD_URL, LSPOSED_CHECKSUM)) {
rename { "lsposed.zip" }
}
from(downloadFile(SHAMIKO_DOWNLOAD_URL, SHAMIKO_CHECKSUM)) {
rename { "shamiko.zip" }
}
into("src/${this@all.name}/assets")
}
mergeAssetsProvider.configure { dependsOn(dlTask) }
}
}

View File

@ -40,9 +40,12 @@
### Developing Rust ### Developing Rust
First, install [rustup](https://www.rust-lang.org/tools/install), the official Rust toolchain manager. The Magisk NDK package [ONDK](https://github.com/topjohnwu/ondk) (the one installed with `./build.py ndk`) bundles a complete Rust toolchain, so _building_ the Magisk project itself does not require any further configuration. The Magisk NDK package [ONDK](https://github.com/topjohnwu/ondk) (the one installed with `./build.py ndk`) bundles a complete Rust toolchain, so _building_ the Magisk project itself does not require any further configuration. However, if you'd like to work on the Rust codebase with proper support, you'd need some setup as most development tools are built around `rustup`.
However, if you'd like to work on the Rust codebase, it'll be easier if you link ONDK's Rust toolchain in `rustup` and set it as default so several development tools and IDEs will work properly: Let's first setup `rustup` to use our custom ONDK Rust toolchain by default:
- Install [rustup](https://rustup.rs/), the official Rust toolchain manager
- Link the ONDK Rust toolchain and set it as default:
```bash ```bash
# Link the ONDK toolchain with the name "magisk" # Link the ONDK toolchain with the name "magisk"
@ -51,7 +54,7 @@ rustup toolchain link magisk "$ANDROID_HOME/ndk/magisk/toolchains/rust"
rustup default magisk rustup default magisk
``` ```
If you plan to use VSCode, you can then install the [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) plugin and everything should be good to go. If you plan to use Jetbrain IDEs (e.g. [Rustrover](https://www.jetbrains.com/rust/), or its Rust Plugin), we need some additional setup: If you plan to use VSCode, you can then install the [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) plugin and everything should be good to go. If you plan to use Jetbrain IDEs (e.g. [Rustrover](https://www.jetbrains.com/rust/), or its Rust Plugin), due to its poor support with custom toolchains, we need some additional setup:
- Install the official nightly toolchain and add some components. We won't actually use the nightly toolchain for anything other than tricking the IDE to cooperate; the magic happens in the wrapper we setup in the next step. - Install the official nightly toolchain and add some components. We won't actually use the nightly toolchain for anything other than tricking the IDE to cooperate; the magic happens in the wrapper we setup in the next step.

View File

@ -1,14 +1,5 @@
# Magisk Changelog # Magisk Changelog
### v29.0
- [General] Massive internal refactoring and code migration
- [App] Support downloading module zip files with XZ compression
- [App] Disable app animations when system animations are disabled
- [MagiskMount] Support systemlessly deleting files with modules using blank file nodes
- [MagiskInit] Redesign sepolicy patching and injection logic
- [MagiskSU] Better TTY/PTY support
### v28.1 ### v28.1
- [App] Fix stub APK download link - [App] Fix stub APK download link

View File

@ -7,10 +7,9 @@ If you have USB debugging enabled in developer options, connect your phone to th
If unfortunately you do not have USB debugging enabled you can boot using the Safe Mode key combo to cause Magisk to create an empty file named 'disable' in modules directories which disables modules when next booted with Magisk. Most modern Android devices support such a special key combo at boot to enter system Safe Mode as an emergency option, but **please note** that Magisk's key combo detection occurs _earlier_ than system detection so the key combo timing indicated by many online guides may need to be altered to activate Magisk's Safe Mode. (It's possible to activate system Safe Mode but not Magisk Safe Mode and vice versa.) If unfortunately you do not have USB debugging enabled you can boot using the Safe Mode key combo to cause Magisk to create an empty file named 'disable' in modules directories which disables modules when next booted with Magisk. Most modern Android devices support such a special key combo at boot to enter system Safe Mode as an emergency option, but **please note** that Magisk's key combo detection occurs _earlier_ than system detection so the key combo timing indicated by many online guides may need to be altered to activate Magisk's Safe Mode. (It's possible to activate system Safe Mode but not Magisk Safe Mode and vice versa.)
The following details should ensure that modules are properly disabled: The following details should ensure that modules are properly disabled:
1) Many online guides for entering Safe Mode say 'When the animated logo appears, press and hold the volume down button until the system boots' or similar. This may actually be _too late_ for Magisk detection however and result in activating system Safe Mode but modules are not disabled.
1. Many online guides for entering Safe Mode say 'When the animated logo appears, press and hold the volume down button until the system boots' or similar. This may actually be _too late_ for Magisk detection however and result in activating system Safe Mode but modules are not disabled. 2) By pressing the volume down button some seconds before the animation and releasing it as soon as the boot animation appears, Magisk's Safe Mode should be activated without activating system Safe Mode (thus avoiding disabling other device and app settings) and the device should then simply boot to normal system with modules disabled.
2. By pressing the volume down button some seconds before the animation and releasing it as soon as the boot animation appears, Magisk's Safe Mode should be activated without activating system Safe Mode (thus avoiding disabling other device and app settings) and the device should then simply boot to normal system with modules disabled. 3) By pressing the volume down button some seconds before the animation and holding it until the system boots, both Magisk's Safe Mode and system Safe Mode should be activated. Next, after booting back to normal system, modules will be disabled.
3. By pressing the volume down button some seconds before the animation and holding it until the system boots, both Magisk's Safe Mode and system Safe Mode should be activated. Next, after booting back to normal system, modules will be disabled.
### Q: Why is X app detecting root? ### Q: Why is X app detecting root?
@ -21,9 +20,3 @@ Magisk no longer handles root hiding. There are plenty of Magisk/Zygisk modules
When hiding the Magisk app, it will install a "stub" APK that has nothing in it. The only functionality this stub app has is downloading the full Magisk app APK into its internal storage and dynamically loading it. Due to the fact that the APK is literally _empty_, it does not contain the image resource for the app icon. When hiding the Magisk app, it will install a "stub" APK that has nothing in it. The only functionality this stub app has is downloading the full Magisk app APK into its internal storage and dynamically loading it. Due to the fact that the APK is literally _empty_, it does not contain the image resource for the app icon.
When you open the hidden Magisk app, it will offer you the option to create a shortcut in the homescreen (which has both the correct app name and icon) for your convenience. You can also manually ask the app to create the icon in app settings. When you open the hidden Magisk app, it will offer you the option to create a shortcut in the homescreen (which has both the correct app name and icon) for your convenience. You can also manually ask the app to create the icon in app settings.
### Q: How to use Magisk in the emulator?
With the emulator running and accessible via ADB, run `./build.py emulator <path to Magisk APK>` to temporarily install Magisk on to the emulator. The patch is not persistent, meaning Magisk will be lost after a reboot, so re-execute the script to emulate a reboot if required.
The script is only tested on the official Android Virtual Device (AVD) shipped alongside Android Studio; other emulators may work, but the emulator must have SELinux enabled.

View File

@ -124,8 +124,6 @@ If you place a file named `.replace` in any of the folders, instead of merging i
If you want to replace files in `/vendor`, `/product`, or `/system_ext`, please place them under `system/vendor`, `system/product`, and `system/system_ext` respectively. Magisk will transparently handle whether these partitions are in a separate partition or not. If you want to replace files in `/vendor`, `/product`, or `/system_ext`, please place them under `system/vendor`, `system/product`, and `system/system_ext` respectively. Magisk will transparently handle whether these partitions are in a separate partition or not.
If you want to remove a specific file or folder, please place a dummy character device with major number 0 and minor number 0 in the same path. For example, if you want to remove `/system/app/GoogleCamera`, you can `mknod GoogleCamera c 0 0` in `$MODDIR/system/app`.
#### Zygisk #### Zygisk
Zygisk is a feature of Magisk that allows advanced module developers to run code directly in every Android applications' processes before they are specialized and running. For more details about the Zygisk API and building a Zygisk module, please checkout the [Zygisk Module Sample](https://github.com/topjohnwu/zygisk-module-sample) project. Zygisk is a feature of Magisk that allows advanced module developers to run code directly in every Android applications' processes before they are specialized and running. For more details about the Zygisk API and building a Zygisk module, please checkout the [Zygisk Module Sample](https://github.com/topjohnwu/zygisk-module-sample) project.
@ -140,7 +138,7 @@ If your module requires some additional sepolicy patches, please add those rules
## Magisk Module Installer ## Magisk Module Installer
A Magisk module installer is a Magisk module packaged in a zip file that can be flashed in the Magisk app or custom recoveries such as TWRP. The simplest Magisk module installer is just a Magisk module packed as a zip file, in addition to the following files only if the module supports flashing in recovery: A Magisk module installer is a Magisk module packaged in a zip file that can be flashed in the Magisk app or custom recoveries such as TWRP. The simplest Magisk module installer is just a Magisk module packed as a zip file, in addition to the following files:
- `update-binary`: Download the latest [module_installer.sh](https://github.com/topjohnwu/Magisk/blob/master/scripts/module_installer.sh) and rename/copy that script as `update-binary` - `update-binary`: Download the latest [module_installer.sh](https://github.com/topjohnwu/Magisk/blob/master/scripts/module_installer.sh) and rename/copy that script as `update-binary`
- `updater-script`: This file should only contain the string `#MAGISK` - `updater-script`: This file should only contain the string `#MAGISK`
@ -150,7 +148,7 @@ The module installer script will setup the environment, extract the module files
``` ```
module.zip module.zip
├── META-INF <--- Only needed for flashing in recovery ├── META-INF
│ └── com │ └── com
│ └── google │ └── google
│ └── android │ └── android
@ -214,7 +212,7 @@ set_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission>
For convenience, you can also declare a list of folders you want to replace in the variable name `REPLACE`. The module installer script will create the `.replace` file into the folders listed in `REPLACE`. For example: For convenience, you can also declare a list of folders you want to replace in the variable name `REPLACE`. The module installer script will create the `.replace` file into the folders listed in `REPLACE`. For example:
```sh ```
REPLACE=" REPLACE="
/system/app/YouTube /system/app/YouTube
/system/app/Bloatware /system/app/Bloatware
@ -223,17 +221,6 @@ REPLACE="
The list above will result in the following files being created: `$MODPATH/system/app/YouTube/.replace` and `$MODPATH/system/app/Bloatware/.replace`. The list above will result in the following files being created: `$MODPATH/system/app/YouTube/.replace` and `$MODPATH/system/app/Bloatware/.replace`.
For convenience, you can also declare a list of files/folders you want to remove in the variable name `REMOVE`. The module installer script will create the corresponding dummy devices. For example:
```sh
REMOVE="
/system/app/YouTube
/system/fonts/Roboto.ttf
"
```
The list above will result in the following dummy devices being created: `$MODPATH/system/app/YouTube` and `$MODPATH/system/fonts/Roboto.ttf`.
#### Notes #### Notes
- When your module is downloaded with the Magisk app, `update-binary` will be **forcefully** replaced with the latest [`module_installer.sh`](https://github.com/topjohnwu/Magisk/blob/master/scripts/module_installer.sh). **DO NOT** try to add any custom logic in `update-binary`. - When your module is downloaded with the Magisk app, `update-binary` will be **forcefully** replaced with the latest [`module_installer.sh`](https://github.com/topjohnwu/Magisk/blob/master/scripts/module_installer.sh). **DO NOT** try to add any custom logic in `update-binary`.

View File

@ -76,7 +76,7 @@ As a summary, after installing Magisk in recovery **(starting from power off)**:
Before proceeding, please acknowledge that: Before proceeding, please acknowledge that:
- Installing Magisk **WILL** trip your Knox Warranty Bit, this action is not reversible in any way. - Installing Magisk **WILL** trip your Knox Warranty Bit, this action is not reversible in any way.
- Installing Magisk for the first time **REQUIRES** a full data wipe (this is **NOT** counting the data wipe when unlocking bootloader). Please make a backup of your data. - Installing Magisk for the first time **REQUIRES** a full data wipe (this is **NOT** counting the data wipe when unlocking bootloader). Please make a backup your data.
### Flashing Tools ### Flashing Tools
@ -86,7 +86,7 @@ Before proceeding, please acknowledge that:
### Requirements ### Requirements
To verify whether or not Magisk can be installed in your Samsung device, you first must check the OEM Lock and KnoxGuard (RMM) status. To do so, boot your device in Download mode with its key combo. To verify whether or not Magisk can be installed in your Samsung device, you first must check the OEM Lock and KnoxGuard (RMM) status, to do so boot your device in Download mode with its key combo.
Possible OEM Lock values are the following: Possible OEM Lock values are the following:
- **ON (L)**: fully locked. - **ON (L)**: fully locked.

View File

@ -1,16 +0,0 @@
## 2025.5.14 Magisk v29.0
This release looks minor at the surface, however, the entire codebase has gone through significant refactoring and migration. The native code in Magisk used to be mainly C++, but several contributors and I have been steadily rewriting parts of the code in Rust since April 2022. After years of effort, the Rust-ification of the project slowly began picking up steam, and at the moment of this release, over 40% of the native code has been rewritten in Rust, with several major subsystem rewrites in the PR queue, planned to be merged for the next release.
Many might wonder, why introduce a new language to the project? My reason is actually not to reduce memory safety issues (although it is a nice side benefit), but to be able to develop Magisk using a more modern programming language. After using Rust for a while, it's clear to me that using Rust allows me to write more correct code and makes me happier compared to dealing with C++. People share the [same sentiment as I do](https://threadreaderapp.com/thread/1577667445719912450.html).
## Changelog
- [General] Massive internal refactoring and code migration
- [App] Support downloading module zip files with XZ compression
- [App] Disable app animations when system animations are disabled
- [MagiskMount] Support systemlessly deleting files with modules using blank file nodes
- [MagiskInit] Redesign sepolicy patching and injection logic
- [MagiskSU] Better TTY/PTY support
### Full Changelog: [here](https://topjohnwu.github.io/Magisk/changes.html)

Some files were not shown because too many files have changed in this diff Show More