mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-12-11 22:42:24 +00:00
Compare commits
492 Commits
canary-270
...
v30.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3db2e368d | ||
|
|
d37da5ca66 | ||
|
|
aac52176ed | ||
|
|
78e2fc37e5 | ||
|
|
ca2e40593f | ||
|
|
c07fdc87e3 | ||
|
|
7270f5e413 | ||
|
|
07cc85ccb1 | ||
|
|
d6f17c42d5 | ||
|
|
d60806f429 | ||
|
|
8836a09c8c | ||
|
|
f16e93c7db | ||
|
|
1b0ddec66e | ||
|
|
cd8820f563 | ||
|
|
b70192ca3e | ||
|
|
d42ec5da9a | ||
|
|
742913ebcb | ||
|
|
ed206c6480 | ||
|
|
f9a8052583 | ||
|
|
f4fdd516f9 | ||
|
|
5925a71f94 | ||
|
|
3cda9beb93 | ||
|
|
8b7d1ffcdd | ||
|
|
8d02d0632e | ||
|
|
dd743f6f7e | ||
|
|
cf483ad4d2 | ||
|
|
4aed644e08 | ||
|
|
0acc39cec0 | ||
|
|
8b3a44344f | ||
|
|
8b49eda85a | ||
|
|
7057d4c7f1 | ||
|
|
aab8344058 | ||
|
|
7cccf83b37 | ||
|
|
f10ad93c4e | ||
|
|
f143b5df15 | ||
|
|
71213cc6f4 | ||
|
|
e2a1774e5b | ||
|
|
0222527a1e | ||
|
|
312bfe1bab | ||
|
|
48c62a1dae | ||
|
|
cfc2bcb665 | ||
|
|
94b1ff674f | ||
|
|
111136733a | ||
|
|
c8caaa98f5 | ||
|
|
8d28f10a3f | ||
|
|
177a456d8b | ||
|
|
ef4e230258 | ||
|
|
17082af438 | ||
|
|
1df5b34175 | ||
|
|
ea5fe7525d | ||
|
|
a75c335261 | ||
|
|
3903f42cf6 | ||
|
|
fb0c4ea838 | ||
|
|
bc89c60977 | ||
|
|
bd657c354c | ||
|
|
675b5f9565 | ||
|
|
1b2c43268e | ||
|
|
653730d75e | ||
|
|
d472e9c36e | ||
|
|
484d53ef7e | ||
|
|
c4e2985677 | ||
|
|
42d9f87bc9 | ||
|
|
2e4fa6864c | ||
|
|
e2abb648ac | ||
|
|
3599dcedfb | ||
|
|
ea72666df8 | ||
|
|
bd2a47ba18 | ||
|
|
b861671391 | ||
|
|
e91fc75d86 | ||
|
|
78f5cd55c7 | ||
|
|
9787a69528 | ||
|
|
87b8fe374d | ||
|
|
7b706bb0cb | ||
|
|
c1491b8d2b | ||
|
|
5cbaf2ae11 | ||
|
|
8ebc6207b4 | ||
|
|
7848ee616b | ||
|
|
fd193c3cae | ||
|
|
36d33c7a85 | ||
|
|
5caf28d27c | ||
|
|
2c39d0234d | ||
|
|
c313812129 | ||
|
|
af51880a81 | ||
|
|
db8d832707 | ||
|
|
8dc23d0ead | ||
|
|
b4287700d5 | ||
|
|
8d10ab89f2 | ||
|
|
49fdc1addb | ||
|
|
1333d3b986 | ||
|
|
335146a6a2 | ||
|
|
eaf9527971 | ||
|
|
da937a88c8 | ||
|
|
9476e7282d | ||
|
|
251c3c3e0e | ||
|
|
cd0eca20b0 | ||
|
|
6839cb9ab2 | ||
|
|
d11a3397d8 | ||
|
|
975120d6a6 | ||
|
|
e489b3b6dd | ||
|
|
589a270b8d | ||
|
|
7961be5cfa | ||
|
|
959430e030 | ||
|
|
2923c8ccd1 | ||
|
|
7df4a9d74f | ||
|
|
bf4ed295da | ||
|
|
a5fca960dc | ||
|
|
f99912b9db | ||
|
|
a54bdb54e4 | ||
|
|
cd9851a1fe | ||
|
|
9ca469898c | ||
|
|
0665549473 | ||
|
|
9d7a14b335 | ||
|
|
62e29fee74 | ||
|
|
e472db552b | ||
|
|
466e4bd4e1 | ||
|
|
4cf525c588 | ||
|
|
c8aec2510d | ||
|
|
ccbfe0e66e | ||
|
|
23ea28de6f | ||
|
|
55c3ee3a6f | ||
|
|
2a42ca2b8f | ||
|
|
a897e82fa4 | ||
|
|
ffa15831d3 | ||
|
|
a344ebf28c | ||
|
|
78f7fa348e | ||
|
|
d8c448b99d | ||
|
|
d4b83b6a44 | ||
|
|
e5d36d1d24 | ||
|
|
ff18cb8e70 | ||
|
|
37a9724a54 | ||
|
|
d660401063 | ||
|
|
88541d6f49 | ||
|
|
ecd6129fe5 | ||
|
|
6dfe9df9e2 | ||
|
|
e81de7ec36 | ||
|
|
c78da1ce24 | ||
|
|
7b2d40987c | ||
|
|
3a37e8c9c5 | ||
|
|
58b405bce1 | ||
|
|
810174ef73 | ||
|
|
690a5ac033 | ||
|
|
89aad31f7e | ||
|
|
7124db98e3 | ||
|
|
0860e859f7 | ||
|
|
04008949b8 | ||
|
|
39f2940bd1 | ||
|
|
1460317ebd | ||
|
|
12340c9bd5 | ||
|
|
c4590fe2ba | ||
|
|
b36066bbcd | ||
|
|
65d1c5827c | ||
|
|
1d675c8b2e | ||
|
|
0b494ed7df | ||
|
|
48d9fc24eb | ||
|
|
83426f7f36 | ||
|
|
0e86d4dbcb | ||
|
|
5e050d7456 | ||
|
|
898580bf90 | ||
|
|
86621a4c46 | ||
|
|
a834e72b71 | ||
|
|
d8cf42af16 | ||
|
|
8c79d66b7b | ||
|
|
fada8b148a | ||
|
|
dc0acea47c | ||
|
|
78d1200608 | ||
|
|
527bbc0368 | ||
|
|
4c89c7e2b3 | ||
|
|
adbea7e313 | ||
|
|
76962f965e | ||
|
|
a4b8c5e46b | ||
|
|
83c707439c | ||
|
|
25dd6121f4 | ||
|
|
67f35ad027 | ||
|
|
0c4b8afbc5 | ||
|
|
34b30d7ce1 | ||
|
|
2215088973 | ||
|
|
8b7fb6cdde | ||
|
|
94c7dbedf2 | ||
|
|
b1dc47a047 | ||
|
|
62b1310d97 | ||
|
|
0a86916d3a | ||
|
|
9907ce57aa | ||
|
|
b92626cacc | ||
|
|
5a762f0a8e | ||
|
|
5dd7a7d804 | ||
|
|
7831f40691 | ||
|
|
4f4b1ff885 | ||
|
|
97901979dd | ||
|
|
287316842c | ||
|
|
608786e8f3 | ||
|
|
9684a35cab | ||
|
|
e3e4202954 | ||
|
|
23c2054d46 | ||
|
|
a20a2a8fa0 | ||
|
|
a2896be4a6 | ||
|
|
e9220a28d9 | ||
|
|
cf12087e21 | ||
|
|
00c1b36837 | ||
|
|
03e034795d | ||
|
|
79c0fafe43 | ||
|
|
d499819ba0 | ||
|
|
86da917174 | ||
|
|
30bd7d6555 | ||
|
|
e5a12f0f5f | ||
|
|
c85a8434c6 | ||
|
|
427a1ca4e5 | ||
|
|
22884e173a | ||
|
|
d1829308e9 | ||
|
|
73840f8721 | ||
|
|
c7d1af9805 | ||
|
|
4ad26d3dfb | ||
|
|
0c70b7670c | ||
|
|
f44d044095 | ||
|
|
5c1cb13472 | ||
|
|
3327fc668e | ||
|
|
610945ac54 | ||
|
|
ddf5474917 | ||
|
|
6ba1685ade | ||
|
|
e02b5f7868 | ||
|
|
ab2e5d1e7e | ||
|
|
f3fef7bfe4 | ||
|
|
c34c7838bb | ||
|
|
c8a16b0e0c | ||
|
|
14f9ed91a1 | ||
|
|
7a207d4ccf | ||
|
|
92a42d901f | ||
|
|
084d89fcce | ||
|
|
55b036c071 | ||
|
|
30e79310ab | ||
|
|
f063fa5054 | ||
|
|
7bd901273c | ||
|
|
c1e061603b | ||
|
|
cb08504fe5 | ||
|
|
c0a1fb77be | ||
|
|
4864c1112a | ||
|
|
9ddeab034b | ||
|
|
c4847ed288 | ||
|
|
b8f1523fb2 | ||
|
|
fb7fa8a6b3 | ||
|
|
9c7d359093 | ||
|
|
eb54bc1fd7 | ||
|
|
d4a0286e13 | ||
|
|
83e66767ff | ||
|
|
7dc010749b | ||
|
|
8e8d013b1b | ||
|
|
bba0373808 | ||
|
|
1fa318dc8c | ||
|
|
6edc5e2037 | ||
|
|
1523ed9f78 | ||
|
|
8e604d2ab8 | ||
|
|
2aba7247a9 | ||
|
|
e66fe8533e | ||
|
|
b03fbb3917 | ||
|
|
c2ece62e4c | ||
|
|
8c972dcf34 | ||
|
|
50af14f2a3 | ||
|
|
e0a356b319 | ||
|
|
c09a792958 | ||
|
|
0bbfe7f44d | ||
|
|
a396abf565 | ||
|
|
1e3edb8883 | ||
|
|
3b8b61bf35 | ||
|
|
6f90456036 | ||
|
|
f56fd4e215 | ||
|
|
aa35aac5d5 | ||
|
|
1f162b819d | ||
|
|
52ef1d1cb2 | ||
|
|
f14e3a89cc | ||
|
|
95d3eac2e0 | ||
|
|
8e73536e02 | ||
|
|
12a0870bc9 | ||
|
|
6ff82c4e86 | ||
|
|
c64de35375 | ||
|
|
ee5283f4e8 | ||
|
|
bd0e954fea | ||
|
|
675471a49e | ||
|
|
c90e73ccec | ||
|
|
a43c1267d8 | ||
|
|
e8958c6b5c | ||
|
|
e8a3bf82c6 | ||
|
|
27fd79176a | ||
|
|
28d86a3454 | ||
|
|
c6c1a17ae6 | ||
|
|
2b47d47215 | ||
|
|
0e82df9e10 | ||
|
|
893821ad88 | ||
|
|
6b80fbfa99 | ||
|
|
8c3c7d0194 | ||
|
|
b94a3d9f2f | ||
|
|
442d0b5ddc | ||
|
|
494615d9a0 | ||
|
|
afbfb81837 | ||
|
|
3ed4e258a3 | ||
|
|
dddd41c95b | ||
|
|
5f2ca81e86 | ||
|
|
c9eac0c438 | ||
|
|
b6b34f7612 | ||
|
|
e55c413261 | ||
|
|
0399cde50a | ||
|
|
019eb03823 | ||
|
|
363410e1c0 | ||
|
|
fc2ef21660 | ||
|
|
18cb659ff3 | ||
|
|
63231d97ce | ||
|
|
9ac81a8a25 | ||
|
|
79af2787ae | ||
|
|
f5f9b285c0 | ||
|
|
6c05f2ae85 | ||
|
|
29043e1684 | ||
|
|
b73d4a7022 | ||
|
|
ad95e8951b | ||
|
|
bf591fca12 | ||
|
|
dcf027884d | ||
|
|
584f3820fe | ||
|
|
3c7c46307a | ||
|
|
4d80361805 | ||
|
|
9a74e19117 | ||
|
|
b1e17706a4 | ||
|
|
caad129d69 | ||
|
|
da58571ce5 | ||
|
|
2aa7f1c094 | ||
|
|
823e31a91b | ||
|
|
fb926ae302 | ||
|
|
e0489eeffd | ||
|
|
dc9d5a4cac | ||
|
|
143743d0b0 | ||
|
|
563f0d5ad5 | ||
|
|
c99f4a591b | ||
|
|
449204e380 | ||
|
|
a85c4c6528 | ||
|
|
d203a6fff6 | ||
|
|
6c612d66d7 | ||
|
|
540253a55b | ||
|
|
15b7c4ccd1 | ||
|
|
442d5335ea | ||
|
|
8a80eea597 | ||
|
|
5e35703091 | ||
|
|
b7ca73f431 | ||
|
|
a14fc90f07 | ||
|
|
c913f7ec74 | ||
|
|
7f6c9e8411 | ||
|
|
bb02ea3a20 | ||
|
|
3981c9665e | ||
|
|
88628fdf3c | ||
|
|
0469817781 | ||
|
|
a786801141 | ||
|
|
ab86732c89 | ||
|
|
59622d1688 | ||
|
|
58a25a3e2b | ||
|
|
15dca29a87 | ||
|
|
46980819c0 | ||
|
|
4fb6a7268c | ||
|
|
c05e963f37 | ||
|
|
7f7f625864 | ||
|
|
b25aa8295a | ||
|
|
15a605765c | ||
|
|
b575c95710 | ||
|
|
a48a9c858a | ||
|
|
0d8d6290a3 | ||
|
|
4dcd733ddd | ||
|
|
b62835cbeb | ||
|
|
6ea740b5ab | ||
|
|
7ab98dd5ac | ||
|
|
fc8b3400fc | ||
|
|
54428ba415 | ||
|
|
95d1e69d8e | ||
|
|
a0f13ab49f | ||
|
|
c3e8405020 | ||
|
|
a93593ea66 | ||
|
|
23eff70883 | ||
|
|
110dd4a8b9 | ||
|
|
d9c2bffc9f | ||
|
|
049db49dc8 | ||
|
|
7c1d2ec61e | ||
|
|
a1b2830c06 | ||
|
|
82d1d19267 | ||
|
|
4d4195c02d | ||
|
|
5637a258fc | ||
|
|
ee6810f417 | ||
|
|
7098248c64 | ||
|
|
0d31d356ef | ||
|
|
b782e7dcb7 | ||
|
|
a4671b4698 | ||
|
|
7edd8be169 | ||
|
|
24650eefe4 | ||
|
|
8e1a44e7eb | ||
|
|
2722875190 | ||
|
|
3ca6d06f69 | ||
|
|
10e47248de | ||
|
|
e73ff679ac | ||
|
|
53e401fa2d | ||
|
|
d2768357da | ||
|
|
a6c2ba7c1e | ||
|
|
aae5b466fb | ||
|
|
2b7be8b949 | ||
|
|
b6511a510d | ||
|
|
704541aef2 | ||
|
|
005560a4c5 | ||
|
|
231a5d1853 | ||
|
|
9e2b59060d | ||
|
|
08ea937f7c | ||
|
|
2baedf74d1 | ||
|
|
32faa4ced6 | ||
|
|
ccdb0b5d13 | ||
|
|
8506b672ad | ||
|
|
ce2e33bb20 | ||
|
|
6707b72260 | ||
|
|
5885b8c20d | ||
|
|
820710c086 | ||
|
|
51cf196bf7 | ||
|
|
dadba44cf9 | ||
|
|
2ce4a5543b | ||
|
|
9112a3a4f5 | ||
|
|
24615afda1 | ||
|
|
c5778f398b | ||
|
|
4eb4097b9b | ||
|
|
c512496847 | ||
|
|
506961a10d | ||
|
|
3414415907 | ||
|
|
dc2ae7cfd1 | ||
|
|
2e86d21c29 | ||
|
|
2654382c43 | ||
|
|
9e26b73813 | ||
|
|
10cd13bf80 | ||
|
|
f10ee5f887 | ||
|
|
47cc532d96 | ||
|
|
218327f92b | ||
|
|
4eae66a1a7 | ||
|
|
b09ceeb43c | ||
|
|
4fb539c110 | ||
|
|
849b284da5 | ||
|
|
895b5f6cbf | ||
|
|
cb3d4ea514 | ||
|
|
0d89a2a97d | ||
|
|
3ca5913055 | ||
|
|
df6b808f49 | ||
|
|
09c7ac754b | ||
|
|
805da67c23 | ||
|
|
3c6889505b | ||
|
|
c8e9ce7627 | ||
|
|
837c679a31 | ||
|
|
06616659b8 | ||
|
|
a34c04f999 | ||
|
|
da43ac89a0 | ||
|
|
830fc758b9 | ||
|
|
0f3cfef278 | ||
|
|
b32d7bfafd | ||
|
|
c0899f2939 | ||
|
|
082330808f | ||
|
|
024da05888 | ||
|
|
377b6d0cc2 | ||
|
|
c661009b31 | ||
|
|
613f2d31c5 | ||
|
|
7dbb973db5 | ||
|
|
f4502f8be8 | ||
|
|
455b13b83c | ||
|
|
8b98709743 | ||
|
|
1b12f45f39 | ||
|
|
a5cad532ff | ||
|
|
070719db50 | ||
|
|
28cccdf7aa | ||
|
|
b7e0986a5c | ||
|
|
da709745dd | ||
|
|
8b6771d487 | ||
|
|
e1b847fbc5 | ||
|
|
7188de1205 | ||
|
|
44fb7dbcbe | ||
|
|
8086b5933c | ||
|
|
7f675f4bf7 | ||
|
|
5e6b53e0da | ||
|
|
5b29fefc65 | ||
|
|
16a168535d | ||
|
|
33f70f8f6d | ||
|
|
4f18a66d73 | ||
|
|
250dc16007 | ||
|
|
7af273e047 | ||
|
|
e381aebaa0 | ||
|
|
45d91c9658 | ||
|
|
4a9158f667 | ||
|
|
0d9ee89e7f | ||
|
|
abaff72304 | ||
|
|
b828e2d0b2 | ||
|
|
53d7cbc11b | ||
|
|
310be7ab47 | ||
|
|
60894e458f | ||
|
|
fbebb6ac10 | ||
|
|
a9f8c20703 | ||
|
|
ae0b15d197 | ||
|
|
869aa62328 | ||
|
|
dcd3bc58a3 | ||
|
|
a82f17c594 | ||
|
|
b38fd1ca5f |
6
.gitattributes
vendored
6
.gitattributes
vendored
@@ -12,13 +12,11 @@
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
tools/** binary
|
||||
tools/rustup-wrapper/** -binary
|
||||
tools/elf-cleaner/** -binary
|
||||
*.jar binary
|
||||
*.exe binary
|
||||
*.apk binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ttf binary
|
||||
|
||||
# Help GitHub detect languages
|
||||
native/jni/external/** linguist-vendored
|
||||
native/jni/systemproperties/** linguist-language=C++
|
||||
|
||||
31
.github/actions/setup/action.yml
vendored
31
.github/actions/setup/action.yml
vendored
@@ -6,11 +6,11 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up JDK 17
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
java-version: "21"
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v5
|
||||
@@ -26,6 +26,15 @@ runs:
|
||||
|
||||
- name: Cache sccache
|
||||
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:
|
||||
path: .sccache
|
||||
key: sccache-${{ runner.os }}-${{ github.sha }}
|
||||
@@ -36,7 +45,7 @@ runs:
|
||||
env:
|
||||
SCCACHE_DIRECT: false
|
||||
SCCACHE_DIR: ${{ github.workspace }}/.sccache
|
||||
SCCACHE_CACHE_SIZE: 2G
|
||||
SCCACHE_CACHE_SIZE: ${{ inputs.is-asset-build == 'true' && '2G' || '300M' }}
|
||||
SCCACHE_IDLE_TIMEOUT: 0
|
||||
run: |
|
||||
bash $GITHUB_ACTION_PATH/sccache.sh
|
||||
@@ -55,18 +64,18 @@ runs:
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v4
|
||||
if: inputs.is-asset-build == 'true'
|
||||
if: ${{ inputs.is-asset-build == 'true' && github.event_name != 'pull_request' }}
|
||||
with:
|
||||
path: |
|
||||
.gradle/caches
|
||||
.gradle/wrapper
|
||||
!.gradle/caches/build-cache-*
|
||||
key: gradle-cache-${{ hashFiles('gradle/**') }}
|
||||
key: gradle-cache-${{ hashFiles('app/gradle/**') }}
|
||||
restore-keys: gradle-cache-
|
||||
|
||||
- name: Restore Gradle dependencies
|
||||
uses: actions/cache/restore@v4
|
||||
if: inputs.is-asset-build == 'false'
|
||||
if: ${{ inputs.is-asset-build == 'false' || github.event_name == 'pull_request' }}
|
||||
with:
|
||||
path: |
|
||||
.gradle/caches
|
||||
@@ -78,19 +87,17 @@ runs:
|
||||
|
||||
- name: Cache Gradle build cache
|
||||
uses: actions/cache@v4
|
||||
if: inputs.is-asset-build == 'true'
|
||||
if: ${{ inputs.is-asset-build == 'true' && github.event_name != 'pull_request' }}
|
||||
with:
|
||||
path: |
|
||||
.gradle/caches/build-cache-*
|
||||
path: .gradle/caches/build-cache-*
|
||||
key: gradle-build-cache-${{ github.sha }}
|
||||
restore-keys: gradle-build-cache-
|
||||
|
||||
- name: Restore Gradle build cache
|
||||
uses: actions/cache/restore@v4
|
||||
if: inputs.is-asset-build == 'false'
|
||||
if: ${{ inputs.is-asset-build == 'false' || github.event_name == 'pull_request' }}
|
||||
with:
|
||||
path: |
|
||||
.gradle/caches/build-cache-*
|
||||
path: .gradle/caches/build-cache-*
|
||||
key: gradle-build-cache-${{ github.sha }}
|
||||
restore-keys: gradle-build-cache-
|
||||
enableCrossOsArchive: true
|
||||
|
||||
43
.github/workflows/build.yml
vendored
43
.github/workflows/build.yml
vendored
@@ -6,9 +6,7 @@ on:
|
||||
paths:
|
||||
- "app/**"
|
||||
- "native/**"
|
||||
- "buildSrc/**"
|
||||
- "build.py"
|
||||
- "gradle.properties"
|
||||
- ".github/workflows/build.yml"
|
||||
pull_request:
|
||||
branches: [master]
|
||||
@@ -17,7 +15,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build Magisk artifacts
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
@@ -38,7 +36,7 @@ jobs:
|
||||
run: ./build.py -v all
|
||||
|
||||
- name: Stop gradle daemon
|
||||
run: ./gradlew --stop
|
||||
run: ./app/gradlew --stop
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -60,7 +58,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
os: [windows-2025, ubuntu-24.04]
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
@@ -74,20 +72,21 @@ jobs:
|
||||
run: python build.py -v -c .github/ci.prop all
|
||||
|
||||
- name: Stop gradle daemon
|
||||
run: ./gradlew --stop
|
||||
run: ./app/gradlew --stop
|
||||
|
||||
avd-test:
|
||||
name: Test API ${{ matrix.version }} (x86_64)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: [23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34]
|
||||
version: [23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, "CANARY"]
|
||||
type: [""]
|
||||
include:
|
||||
- version: 35
|
||||
type: "google_apis"
|
||||
- version: "CANARY"
|
||||
type: "google_apis_ps16k"
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
@@ -106,10 +105,10 @@ jobs:
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Run AVD test
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
AVD_TEST_LOG: 1
|
||||
run: scripts/avd_test.sh ${{ matrix.version }} ${{ matrix.type }}
|
||||
run: scripts/avd.sh test ${{ matrix.version }} ${{ matrix.type }}
|
||||
|
||||
- name: Upload logs on error
|
||||
if: ${{ failure() }}
|
||||
@@ -122,8 +121,9 @@ jobs:
|
||||
|
||||
avd-test-32:
|
||||
name: Test API ${{ matrix.version }} (x86)
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -146,11 +146,11 @@ jobs:
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Run AVD test
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
FORCE_32_BIT: 1
|
||||
AVD_TEST_LOG: 1
|
||||
run: scripts/avd_test.sh ${{ matrix.version }}
|
||||
run: scripts/avd.sh test ${{ matrix.version }}
|
||||
|
||||
- name: Upload logs on error
|
||||
if: ${{ failure() }}
|
||||
@@ -161,20 +161,19 @@ jobs:
|
||||
kernel.log
|
||||
logcat.log
|
||||
|
||||
cf_test:
|
||||
cf-test:
|
||||
name: Test ${{ matrix.device }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
env:
|
||||
CF_HOME: /home/runner/aosp_cf_phone
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- branch: "aosp-main"
|
||||
device: "aosp_cf_x86_64_phone"
|
||||
- branch: "aosp-main-throttled"
|
||||
device: "aosp_cf_x86_64_phone_pgagnostic"
|
||||
- branch: "aosp-android-latest-release"
|
||||
device: "aosp_cf_x86_64_only_phone"
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
@@ -192,8 +191,8 @@ jobs:
|
||||
scripts/cuttlefish.sh download ${{ matrix.branch }} ${{ matrix.device }}
|
||||
|
||||
- name: Run Cuttlefish test
|
||||
timeout-minutes: 10
|
||||
run: su $USER -c 'scripts/cuttlefish.sh test'
|
||||
timeout-minutes: 15
|
||||
run: sudo -E -u $USER scripts/cuttlefish.sh test
|
||||
|
||||
- name: Upload logs on error
|
||||
if: ${{ failure() }}
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -2,18 +2,13 @@ out
|
||||
*.zip
|
||||
*.jks
|
||||
*.apk
|
||||
*.log
|
||||
/config.prop
|
||||
/notes.md
|
||||
/update.sh
|
||||
/app/dict.txt
|
||||
|
||||
# Built binaries
|
||||
native/out
|
||||
|
||||
# Android Studio / Gradle
|
||||
# Android Studio
|
||||
*.iml
|
||||
.gradle
|
||||
.idea
|
||||
/local.properties
|
||||
/build
|
||||
/captures
|
||||
|
||||
15
.gitmodules
vendored
15
.gitmodules
vendored
@@ -4,21 +4,9 @@
|
||||
[submodule "lz4"]
|
||||
path = native/src/external/lz4
|
||||
url = https://github.com/lz4/lz4.git
|
||||
[submodule "bzip2"]
|
||||
path = native/src/external/bzip2
|
||||
url = https://github.com/nemequ/bzip2.git
|
||||
[submodule "xz"]
|
||||
path = native/src/external/xz
|
||||
url = https://github.com/xz-mirror/xz.git
|
||||
[submodule "libcxx"]
|
||||
path = native/src/external/libcxx
|
||||
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"]
|
||||
path = native/src/external/cxx-rs
|
||||
url = https://github.com/topjohnwu/cxx.git
|
||||
@@ -31,6 +19,3 @@
|
||||
[submodule "crt0"]
|
||||
path = native/src/external/crt0
|
||||
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
|
||||
|
||||
@@ -16,13 +16,7 @@ Some highlight features:
|
||||
|
||||
## Downloads
|
||||
|
||||
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
||||
|
||||
Click the icon below to download Magisk apk.
|
||||
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v27.0)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v27.0)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/canary-27007)
|
||||
[Github](https://github.com/topjohnwu/Magisk/releases) is the only source where you can get official Magisk information and downloads.
|
||||
|
||||
## Useful Links
|
||||
|
||||
|
||||
7
app/.gitignore
vendored
Normal file
7
app/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/dict.txt
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
.kotlin
|
||||
/local.properties
|
||||
/build
|
||||
@@ -6,46 +6,37 @@ plugins {
|
||||
id("androidx.navigation.safeargs.kotlin")
|
||||
}
|
||||
|
||||
setupAppCommon()
|
||||
setupMainApk()
|
||||
|
||||
kapt {
|
||||
correctErrorTypes = true
|
||||
useBuildCache = true
|
||||
mapDiagnosticLocations = true
|
||||
javacOptions {
|
||||
option("-Xmaxerrs", 1000)
|
||||
option("-Xmaxerrs", "1000")
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.topjohnwu.magisk"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.topjohnwu.magisk"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
versionName = Config.version
|
||||
versionCode = Config.versionCode
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64", "riscv64")
|
||||
debugSymbolLevel = "FULL"
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles("proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":app:core"))
|
||||
implementation(project(":core"))
|
||||
coreLibraryDesugaring(libs.jdk.libs)
|
||||
|
||||
implementation(libs.indeterminate.checkbox)
|
||||
implementation(libs.rikka.layoutinflater)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.view.KeyEvent
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDirections
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.navOptions
|
||||
import com.topjohnwu.magisk.utils.AccessibilityUtils
|
||||
|
||||
abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Binding>() {
|
||||
|
||||
@@ -31,7 +34,17 @@ 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() {
|
||||
navigation.navigate(this)
|
||||
navigate(this, navigation, contentResolver)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.core.widget.ImageViewCompat
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.databinding.InverseBindingAdapter
|
||||
import androidx.databinding.InverseBindingListener
|
||||
import androidx.databinding.InverseMethod
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
@@ -33,9 +34,11 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.widget.IndeterminateCheckBox
|
||||
@@ -306,3 +309,38 @@ fun TextView.setText(text: TextHolder) {
|
||||
fun Spinner.setAdapter(items: Array<Any>, layoutRes: Int) {
|
||||
adapter = ArrayAdapter(context, layoutRes, items)
|
||||
}
|
||||
|
||||
@BindingAdapter("labelFormatter")
|
||||
fun Slider.setLabelFormatter(formatter: (Float) -> Int) {
|
||||
setLabelFormatter { value -> resources.getString(formatter(value)) }
|
||||
}
|
||||
|
||||
@InverseBindingAdapter(attribute = "android:value")
|
||||
fun Slider.getValueBinding() = value
|
||||
|
||||
@BindingAdapter("android:valueAttrChanged")
|
||||
fun Slider.setListener(attrChange: InverseBindingListener) {
|
||||
addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) = Unit
|
||||
override fun onStopTrackingTouch(slider: Slider) = attrChange.onChange()
|
||||
})
|
||||
}
|
||||
|
||||
@InverseMethod("sliderValueToPolicy")
|
||||
fun policyToSliderValue(policy: Int): Float {
|
||||
return when (policy) {
|
||||
SuPolicy.DENY -> 1f
|
||||
SuPolicy.RESTRICT -> 2f
|
||||
SuPolicy.ALLOW -> 3f
|
||||
else -> 1f
|
||||
}
|
||||
}
|
||||
|
||||
fun sliderValueToPolicy(value: Float): Int {
|
||||
return when (value) {
|
||||
1f -> SuPolicy.DENY
|
||||
2f -> SuPolicy.RESTRICT
|
||||
3f -> SuPolicy.ALLOW
|
||||
else -> SuPolicy.DENY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.ktx.reboot
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class EnvFixDialog(private val vm: HomeViewModel, private val code: Int) : DialogBuilder {
|
||||
@@ -27,9 +32,15 @@ class EnvFixDialog(private val vm: HomeViewModel, private val code: Int) : Dialo
|
||||
setCancelable(false)
|
||||
}
|
||||
dialog.activity.lifecycleScope.launch {
|
||||
MagiskInstaller.FixEnv {
|
||||
MagiskInstaller.FixEnv().exec { success ->
|
||||
dialog.dismiss()
|
||||
}.exec()
|
||||
context.toast(
|
||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
if (success)
|
||||
UiThreadHandler.handler.postDelayed(5000) { reboot() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.topjohnwu.magisk.dialog
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
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.Subject
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
@@ -11,15 +10,10 @@ import java.io.File
|
||||
|
||||
class ManagerInstallDialog : MarkDownDialog() {
|
||||
|
||||
private val svc get() = ServiceLocator.networkService
|
||||
|
||||
override suspend fun getMarkdownText(): String {
|
||||
val text = svc.fetchString(Info.remote.magisk.note)
|
||||
val text = Info.update.note
|
||||
// Cache the changelog
|
||||
AppContext.cacheDir.listFiles { _, name -> name.endsWith(".md") }.orEmpty().forEach {
|
||||
it.delete()
|
||||
}
|
||||
File(AppContext.cacheDir, "${Info.remote.magisk.versionCode}.md").writeText(text)
|
||||
File(AppContext.cacheDir, "${Info.update.versionCode}.md").writeText(text)
|
||||
return text
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||
import com.topjohnwu.magisk.arch.UIActivity
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UninstallDialog : DialogBuilder {
|
||||
|
||||
@@ -19,7 +21,7 @@ class UninstallDialog : DialogBuilder {
|
||||
setMessage(R.string.uninstall_magisk_msg)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = R.string.restore_img
|
||||
onClick { restore(dialog.context) }
|
||||
onClick { restore(dialog.activity) }
|
||||
}
|
||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
text = R.string.complete_uninstall
|
||||
@@ -29,18 +31,20 @@ class UninstallDialog : DialogBuilder {
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun restore(context: Context) {
|
||||
val dialog = ProgressDialog(context).apply {
|
||||
setMessage(context.getString(R.string.restore_img_msg))
|
||||
private fun restore(activity: UIActivity<*>) {
|
||||
val dialog = ProgressDialog(activity).apply {
|
||||
setMessage(activity.getString(R.string.restore_img_msg))
|
||||
show()
|
||||
}
|
||||
|
||||
Shell.cmd("restore_imgs").submit { result ->
|
||||
activity.lifecycleScope.launch {
|
||||
MagiskInstaller.Restore().exec { success ->
|
||||
dialog.dismiss()
|
||||
if (result.isSuccess) {
|
||||
context.toast(R.string.restore_done, Toast.LENGTH_SHORT)
|
||||
if (success) {
|
||||
activity.toast(R.string.restore_done, Toast.LENGTH_SHORT)
|
||||
} else {
|
||||
context.toast(R.string.restore_fail, Toast.LENGTH_LONG)
|
||||
activity.toast(R.string.restore_fail, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class FlashFragment : BaseFragment<FragmentFlashMd2Binding>(), MenuProvider {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
defaultOrientation = activity?.requestedOrientation ?: -1
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
if (savedInstanceState == null) {
|
||||
viewModel.startFlashing()
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
import androidx.navigation.findNavController
|
||||
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||
|
||||
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
|
||||
|
||||
@@ -68,7 +70,13 @@ class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_settings ->
|
||||
HomeFragmentDirections.actionHomeFragmentToSettingsFragment().navigate()
|
||||
activity?.let {
|
||||
NavigationActivity.navigate(
|
||||
HomeFragmentDirections.actionHomeFragmentToSettingsFragment(),
|
||||
it.findNavController(R.id.main_nav_host),
|
||||
it.contentResolver,
|
||||
)
|
||||
}
|
||||
R.id.action_reboot -> activity?.let { RebootMenu.inflate(it).show() }
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
@@ -91,16 +91,15 @@ class HomeViewModel(
|
||||
|
||||
override suspend fun doLoadWork() {
|
||||
appState = State.LOADING
|
||||
Info.getRemote(svc)?.apply {
|
||||
Info.fetchUpdate(svc)?.apply {
|
||||
appState = when {
|
||||
BuildConfig.APP_VERSION_CODE < magisk.versionCode -> State.OUTDATED
|
||||
BuildConfig.APP_VERSION_CODE < versionCode -> State.OUTDATED
|
||||
else -> State.UP_TO_DATE
|
||||
}
|
||||
|
||||
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
|
||||
managerRemoteVersion =
|
||||
("${magisk.version} (${magisk.versionCode})" +
|
||||
if (isDebug) " (D)" else "").asText()
|
||||
("$version (${versionCode})" + if (isDebug) " (D)" else "").asText()
|
||||
} ?: run {
|
||||
appState = State.INVALID
|
||||
managerRemoteVersion = CoreR.string.not_available.asText()
|
||||
|
||||
@@ -41,7 +41,7 @@ object RebootMenu {
|
||||
activity.getSystemService<PowerManager>()?.isRebootingUserspaceSupported == true) {
|
||||
menu.menu.findItem(R.id.action_reboot_userspace).isVisible = true
|
||||
}
|
||||
if (Const.Version.isCanary()) {
|
||||
if (Const.Version.atLeast_28_0()) {
|
||||
menu.menu.findItem(R.id.action_reboot_safe_mode).isChecked = Config.bootloop >= 2
|
||||
} else {
|
||||
menu.menu.findItem(R.id.action_reboot_safe_mode).isVisible = false
|
||||
|
||||
@@ -14,9 +14,8 @@ import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
@@ -70,17 +69,17 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val file = File(AppContext.cacheDir, "${BuildConfig.APP_VERSION_CODE}.md")
|
||||
val text = when {
|
||||
file.exists() -> file.readText()
|
||||
Const.Url.CHANGELOG_URL.isEmpty() -> ""
|
||||
val noteFile = File(AppContext.cacheDir, "${APP_VERSION_CODE}.md")
|
||||
val noteText = when {
|
||||
noteFile.exists() -> noteFile.readText()
|
||||
else -> {
|
||||
val str = svc.fetchString(Const.Url.CHANGELOG_URL)
|
||||
file.writeText(str)
|
||||
str
|
||||
val note = svc.fetchUpdate(APP_VERSION_CODE)?.note.orEmpty()
|
||||
if (note.isEmpty()) return@launch
|
||||
noteFile.writeText(note)
|
||||
note
|
||||
}
|
||||
}
|
||||
val spanned = markwon.toMarkdown(text)
|
||||
val spanned = markwon.toMarkdown(noteText)
|
||||
withContext(Dispatchers.Main) {
|
||||
notes = spanned
|
||||
}
|
||||
@@ -100,13 +99,15 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
|
||||
}
|
||||
|
||||
override fun onSaveState(state: Bundle) {
|
||||
state.putParcelable(INSTALL_STATE_KEY, InstallState(
|
||||
state.putParcelable(
|
||||
INSTALL_STATE_KEY, InstallState(
|
||||
methodId,
|
||||
step,
|
||||
Config.keepVerity,
|
||||
Config.keepEnc,
|
||||
Config.recovery
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRestoreState(state: Bundle) {
|
||||
@@ -124,6 +125,7 @@ class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel()
|
||||
override fun onActivityLaunch() {
|
||||
AppContext.toast(CoreR.string.patch_file_msg, Toast.LENGTH_LONG)
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: Uri) {
|
||||
uri.value = result
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.HorizontalScrollView
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isVisible
|
||||
import com.topjohnwu.magisk.R
|
||||
@@ -12,6 +13,7 @@ import com.topjohnwu.magisk.arch.BaseFragment
|
||||
import com.topjohnwu.magisk.arch.viewModel
|
||||
import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.utils.AccessibilityUtils
|
||||
import com.topjohnwu.magisk.utils.MotionRevealHelper
|
||||
import rikka.recyclerview.addEdgeSpacing
|
||||
import rikka.recyclerview.addItemSpacing
|
||||
@@ -56,6 +58,11 @@ class LogFragment : BaseFragment<FragmentLogMd2Binding>(), MenuProvider {
|
||||
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
|
||||
fixEdgeEffect()
|
||||
}
|
||||
|
||||
if (!AccessibilityUtils.isAnimationEnabled(requireContext().contentResolver)) {
|
||||
val scrollView = view.findViewById<HorizontalScrollView>(R.id.log_scroll_magisk)
|
||||
scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isVisible
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseFragment
|
||||
import com.topjohnwu.magisk.arch.viewModel
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.databinding.FragmentActionMd2Binding
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
class ActionFragment : BaseFragment<FragmentActionMd2Binding>(), MenuProvider {
|
||||
|
||||
override val layoutRes = R.layout.fragment_action_md2
|
||||
override val viewModel by viewModel<ActionViewModel>()
|
||||
override val snackbarView: View get() = binding.snackbarContainer
|
||||
|
||||
private var defaultOrientation = -1
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel.args = ActionFragmentArgs.fromBundle(requireArguments())
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
activity?.setTitle(viewModel.args.name)
|
||||
binding.closeBtn.setOnClickListener {
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
|
||||
viewModel.state.observe(this) {
|
||||
if (it != ActionViewModel.State.RUNNING) {
|
||||
binding.closeBtn.apply {
|
||||
if (!this.isVisible) this.show()
|
||||
if (!this.isFocused) this.requestFocus()
|
||||
}
|
||||
}
|
||||
if (it != ActionViewModel.State.SUCCESS) return@observe
|
||||
view?.viewTreeObserver?.addOnWindowFocusChangeListener(
|
||||
object : ViewTreeObserver.OnWindowFocusChangeListener {
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
if (hasFocus) return
|
||||
view?.viewTreeObserver?.removeOnWindowFocusChangeListener(this)
|
||||
view?.context?.apply {
|
||||
toast(
|
||||
getString(CoreR.string.done_action, viewModel.args.name),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
viewModel.back()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_flash, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
return viewModel.onMenuItemClicked(item)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
defaultOrientation = activity?.requestedOrientation ?: -1
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
if (savedInstanceState == null) {
|
||||
viewModel.startRunAction()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
override fun onDestroyView() {
|
||||
if (defaultOrientation != -1) {
|
||||
activity?.requestedOrientation = defaultOrientation
|
||||
}
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onKeyEvent(event: KeyEvent): Boolean {
|
||||
return when (event.keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
if (viewModel.state.value == ActionViewModel.State.RUNNING) return true
|
||||
return super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onPreBind(binding: FragmentActionMd2Binding) = Unit
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import android.view.MenuItem
|
||||
import androidx.databinding.ObservableArrayList
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.ktx.synchronized
|
||||
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.ui.flash.ConsoleItem
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
class ActionViewModel : BaseViewModel() {
|
||||
|
||||
enum class State {
|
||||
RUNNING, SUCCESS, FAILED
|
||||
}
|
||||
|
||||
private val _state = MutableLiveData(State.RUNNING)
|
||||
val state: LiveData<State> get() = _state
|
||||
|
||||
val items = ObservableArrayList<ConsoleItem>()
|
||||
lateinit var args: ActionFragmentArgs
|
||||
|
||||
private val logItems = mutableListOf<String>().synchronized()
|
||||
private val outItems = object : CallbackList<String>() {
|
||||
override fun onAddElement(e: String?) {
|
||||
e ?: return
|
||||
items.add(ConsoleItem(e))
|
||||
logItems.add(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun startRunAction() = viewModelScope.launch {
|
||||
onResult(withContext(Dispatchers.IO) {
|
||||
try {
|
||||
Shell.cmd("run_action \'${args.id}\'")
|
||||
.to(outItems, logItems)
|
||||
.exec().isSuccess
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun onResult(success: Boolean) {
|
||||
_state.value = if (success) State.SUCCESS else State.FAILED
|
||||
}
|
||||
|
||||
fun onMenuItemClicked(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_save -> savePressed()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun savePressed() = withExternalRW {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val name = "%s_action_log_%s.log".format(
|
||||
args.name,
|
||||
System.currentTimeMillis().toTime(timeFormatStandard)
|
||||
)
|
||||
val file = MediaStoreUtils.getFile(name)
|
||||
file.uri.outputStream().bufferedWriter().use { writer ->
|
||||
synchronized(logItems) {
|
||||
logItems.forEach {
|
||||
writer.write(it)
|
||||
writer.newLine()
|
||||
}
|
||||
}
|
||||
}
|
||||
SnackbarEvent(file.toString()).publish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ class LocalModuleRvItem(
|
||||
override val layoutRes = R.layout.item_module_md2
|
||||
|
||||
val showNotice: Boolean
|
||||
val showAction: Boolean
|
||||
val noticeText: TextHolder
|
||||
|
||||
init {
|
||||
@@ -35,6 +36,7 @@ class LocalModuleRvItem(
|
||||
showNotice = zygiskUnloaded ||
|
||||
(Info.isZygiskEnabled && isRiru) ||
|
||||
(!Info.isZygiskEnabled && isZygisk)
|
||||
showAction = item.hasAction && !showNotice
|
||||
noticeText =
|
||||
when {
|
||||
zygiskUnloaded -> CoreR.string.zygisk_module_unloaded.asText()
|
||||
|
||||
@@ -4,8 +4,10 @@ import android.net.Uri
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.MainDirections
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
@@ -96,6 +98,10 @@ class ModuleViewModel : AsyncLoadViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun runAction(id: String, name: String) {
|
||||
MainDirections.actionActionFragment(id, name).navigate()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val uri = MutableLiveData<Uri?>()
|
||||
}
|
||||
|
||||
@@ -147,19 +147,11 @@ object UpdateChannel : BaseSettingsItem.Selector() {
|
||||
get() = Config.updateChannel
|
||||
set(value) {
|
||||
Config.updateChannel = value
|
||||
Info.remote = Info.EMPTY_REMOTE
|
||||
Info.resetUpdate()
|
||||
}
|
||||
|
||||
override val title = CoreR.string.settings_update_channel_title.asText()
|
||||
|
||||
override val entryRes = CoreR.array.update_channel
|
||||
override fun entries(res: Resources): Array<String> {
|
||||
return super.entries(res).let {
|
||||
if (!Const.APP_IS_CANARY && !BuildConfig.DEBUG)
|
||||
it.copyOfRange(0, Config.Value.CANARY_CHANNEL)
|
||||
else it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UpdateChannelUrl : BaseSettingsItem.Input() {
|
||||
@@ -169,7 +161,7 @@ object UpdateChannelUrl : BaseSettingsItem.Input() {
|
||||
get() = Config.customChannelUrl
|
||||
set(value) {
|
||||
Config.customChannelUrl = value
|
||||
Info.remote = Info.EMPTY_REMOTE
|
||||
Info.resetUpdate()
|
||||
notifyPropertyChanged(BR.description)
|
||||
}
|
||||
|
||||
@@ -330,6 +322,12 @@ object Reauthenticate : BaseSettingsItem.Toggle() {
|
||||
override var value by Config::suReAuth
|
||||
|
||||
override fun refresh() {
|
||||
isEnabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Info.showSuperUser
|
||||
isEnabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||
}
|
||||
}
|
||||
|
||||
object Restrict : BaseSettingsItem.Toggle() {
|
||||
override val title = CoreR.string.settings_su_restrict_title.asText()
|
||||
override val description = CoreR.string.settings_su_restrict_summary.asText()
|
||||
override var value by Config::suRestrict
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.R
|
||||
@@ -21,11 +22,11 @@ import com.topjohnwu.magisk.core.ktx.activity
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.tasks.AppMigration
|
||||
import com.topjohnwu.magisk.core.utils.LocaleSetting
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.events.AddHomeIconEvent
|
||||
import com.topjohnwu.magisk.events.AuthEvent
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
|
||||
@@ -82,6 +83,9 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
|
||||
// Can hide overlay windows on 12.0+
|
||||
list.remove(Tapjack)
|
||||
}
|
||||
if (Const.Version.atLeast_30_1()) {
|
||||
list.add(Restrict)
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
@@ -92,7 +96,7 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
|
||||
DownloadPath -> withExternalRW(doAction)
|
||||
UpdateChecker -> withPostNotificationPermission(doAction)
|
||||
Authentication -> AuthEvent(doAction).publish()
|
||||
Hide, Restore -> withInstallPermission(doAction)
|
||||
AutomaticResponse -> if (Config.suAuth) AuthEvent(doAction).publish() else doAction()
|
||||
else -> doAction()
|
||||
}
|
||||
}
|
||||
@@ -126,7 +130,8 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
|
||||
}
|
||||
|
||||
private fun createHosts() {
|
||||
Shell.cmd("add_hosts_module").submit {
|
||||
viewModelScope.launch {
|
||||
RootUtils.addSystemlessHosts()
|
||||
AppContext.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import android.graphics.drawable.Drawable
|
||||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.ItemWrapper
|
||||
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
class PolicyRvItem(
|
||||
private val viewModel: SuperuserViewModel,
|
||||
@@ -33,12 +35,32 @@ class PolicyRvItem(
|
||||
var isExpanded = false
|
||||
set(value) = set(value, field, { field = it }, BR.expanded)
|
||||
|
||||
val showSlider = Config.suRestrict || item.policy == SuPolicy.RESTRICT
|
||||
|
||||
@get:Bindable
|
||||
var isEnabled
|
||||
get() = item.policy == SuPolicy.ALLOW
|
||||
get() = item.policy >= SuPolicy.ALLOW
|
||||
set(value) = setImpl(value, isEnabled) {
|
||||
notifyPropertyChanged(BR.enabled)
|
||||
viewModel.togglePolicy(this, value)
|
||||
viewModel.updatePolicy(this, if (it) SuPolicy.ALLOW else SuPolicy.DENY)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var sliderValue
|
||||
get() = item.policy
|
||||
set(value) = setImpl(value, sliderValue) {
|
||||
notifyPropertyChanged(BR.sliderValue)
|
||||
notifyPropertyChanged(BR.enabled)
|
||||
viewModel.updatePolicy(this, it)
|
||||
}
|
||||
|
||||
val sliderValueToPolicyString: (Float) -> Int = { value ->
|
||||
when (value.toInt()) {
|
||||
1 -> CoreR.string.deny
|
||||
2 -> CoreR.string.restrict
|
||||
3 -> CoreR.string.grant
|
||||
else -> CoreR.string.deny
|
||||
}
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
|
||||
@@ -78,8 +78,8 @@ class SuperuserViewModel(
|
||||
this@SuperuserViewModel, policy,
|
||||
info.packageName,
|
||||
info.sharedUserId != null,
|
||||
info.applicationInfo.loadIcon(pm),
|
||||
info.applicationInfo.getLabel(pm)
|
||||
info.applicationInfo?.loadIcon(pm) ?: pm.defaultActivityIcon,
|
||||
info.applicationInfo?.getLabel(pm) ?: info.packageName
|
||||
)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
@@ -156,15 +156,16 @@ class SuperuserViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
|
||||
fun updatePolicy(item: PolicyRvItem, policy: Int) {
|
||||
val items = itemsPolicies.filter { it.item.uid == item.item.uid }
|
||||
fun updateState() {
|
||||
viewModelScope.launch {
|
||||
val res = if (enable) R.string.su_snack_grant else R.string.su_snack_deny
|
||||
item.item.policy = if (enable) SuPolicy.ALLOW else SuPolicy.DENY
|
||||
val res = if (policy >= SuPolicy.ALLOW) R.string.su_snack_grant else R.string.su_snack_deny
|
||||
item.item.policy = policy
|
||||
db.update(item.item)
|
||||
items.forEach {
|
||||
it.notifyPropertyChanged(BR.enabled)
|
||||
it.notifyPropertyChanged(BR.sliderValue)
|
||||
}
|
||||
SnackbarEvent(res.asText(item.appName)).publish()
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ class SuRequestViewModel(
|
||||
// shared UID. We have no way to know where this request comes from.
|
||||
icon = pm.defaultActivityIcon
|
||||
title = "[SharedUID] ${info.sharedUserId}"
|
||||
packageName = info.sharedUserId
|
||||
packageName = info.sharedUserId.toString()
|
||||
} else {
|
||||
val prefix = if (info.sharedUserId == null) "" else "[SharedUID] "
|
||||
icon = app.loadIcon(pm)
|
||||
|
||||
@@ -43,10 +43,6 @@ enum class Theme(
|
||||
|
||||
val isSelected get() = Config.themeOrdinal == ordinal
|
||||
|
||||
fun select() {
|
||||
Config.themeOrdinal = ordinal
|
||||
}
|
||||
|
||||
companion object {
|
||||
val selected get() = values().getOrNull(Config.themeOrdinal) ?: Piplup
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
5
app/apk/src/main/res/drawable/ic_action_md2.xml
Normal file
5
app/apk/src/main/res/drawable/ic_action_md2.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
|
||||
|
||||
</vector>
|
||||
68
app/apk/src/main/res/layout/fragment_action_md2.xml
Normal file
68
app/apk/src/main/res/layout/fragment_action_md2.xml
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="com.topjohnwu.magisk.ui.module.ActionViewModel" />
|
||||
|
||||
</data>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="@dimen/internal_action_bar_size"
|
||||
app:layout_fitsSystemWindowsInsets="top"
|
||||
tools:layout_marginTop="@dimen/internal_action_bar_size">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/flash_content"
|
||||
scrollToLast="@{true}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
app:fitsSystemWindowsInsets="start|end|bottom"
|
||||
app:items="@{viewModel.items}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:listitem="@layout/item_console_md2" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/close_btn"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/l1"
|
||||
android:layout_marginBottom="@dimen/l1"
|
||||
android:clickable="true"
|
||||
android:enabled="true"
|
||||
android:focusable="true"
|
||||
android:text="@string/close"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="?colorOnPrimary"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="?colorPrimary"
|
||||
app:icon="@drawable/ic_close_md2"
|
||||
app:iconTint="?colorOnPrimary"
|
||||
app:layout_fitsSystemWindowsInsets="bottom" />
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/snackbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:fitsSystemWindowsInsets="top|bottom" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</layout>
|
||||
@@ -16,6 +16,7 @@
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/log_scroll_magisk"
|
||||
gone="@{viewModel.loading}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<include
|
||||
android:id="@+id/log_track_container"
|
||||
bullet="@{item.log.action == 2 ? R.drawable.ic_check_md2 : R.drawable.ic_close_md2}"
|
||||
bullet="@{item.log.action >= 2 ? R.drawable.ic_check_md2 : R.drawable.ic_close_md2}"
|
||||
isBottom="@{item.isBottom}"
|
||||
isSelected="@{item.log.action != 2}"
|
||||
isTop="@{item.isTop}"
|
||||
|
||||
@@ -189,12 +189,32 @@
|
||||
android:textColor="?colorError"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/module_remove"
|
||||
app:layout_constraintEnd_toStartOf="@+id/bottom_bar_barrier"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/module_config"
|
||||
app:layout_constraintTop_toTopOf="@+id/module_remove"
|
||||
tools:lines="2"
|
||||
tools:text="@tools:sample/lorem/random"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/module_config"
|
||||
style="@style/WidgetFoundation.Button.Text"
|
||||
goneUnless="@{item.showAction}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:enabled="@{item.enabled}"
|
||||
android:focusable="true"
|
||||
android:onClick="@{() -> viewModel.runAction(item.item.id, item.item.name)}"
|
||||
android:text="@string/module_action"
|
||||
android:textAllCaps="false"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_action_md2"
|
||||
app:iconGravity="textStart"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/module_remove"
|
||||
app:layout_constraintTop_toTopOf="@+id/module_remove"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:srcCompat="@drawable/ic_download_md2" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.topjohnwu.magisk.databinding.DataBindingAdaptersKt" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="com.topjohnwu.magisk.ui.superuser.PolicyRvItem" />
|
||||
@@ -85,16 +87,32 @@
|
||||
app:layout_constraintVertical_bias="0"
|
||||
tools:text="com.topjohnwu.magisk" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
<FrameLayout
|
||||
android:id="@+id/policy_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/l1"
|
||||
android:checked="@={item.enabled}"
|
||||
android:nextFocusLeft="@id/policy"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
gone="@{item.showSlider}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="@={item.enabled}" />
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
goneUnless="@{item.showSlider}"
|
||||
labelFormatter="@{item.sliderValueToPolicyString}"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:stepSize="1"
|
||||
android:value="@={DataBindingAdaptersKt.policyToSliderValue(item.sliderValue)}"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="3" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
@@ -53,6 +53,21 @@
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/actionFragment"
|
||||
android:name="com.topjohnwu.magisk.ui.module.ActionFragment"
|
||||
android:label="ActionFragment"
|
||||
tools:layout="@layout/fragment_action_md2" >
|
||||
|
||||
<argument
|
||||
android:name="id"
|
||||
app:argType="string" />
|
||||
|
||||
<argument
|
||||
android:name="name"
|
||||
app:argType="string" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/installFragment"
|
||||
android:name="com.topjohnwu.magisk.ui.install.InstallFragment"
|
||||
@@ -152,4 +167,12 @@
|
||||
app:popEnterAnim="@anim/fragment_enter_pop"
|
||||
app:popExitAnim="@anim/fragment_exit_pop" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_actionFragment"
|
||||
app:destination="@id/actionFragment"
|
||||
app:enterAnim="@anim/fragment_enter"
|
||||
app:exitAnim="@anim/fragment_exit"
|
||||
app:popEnterAnim="@anim/fragment_enter_pop"
|
||||
app:popExitAnim="@anim/fragment_exit_pop" />
|
||||
|
||||
</navigation>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
tasks.register("clean") {
|
||||
plugins {
|
||||
id("MagiskPlugin")
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete::class) {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
|
||||
subprojects.forEach {
|
||||
dependsOn(":app:${it.name}:clean")
|
||||
dependsOn(":${it.name}:clean")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
@@ -18,17 +18,12 @@ gradlePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
languageVersion = "2.0"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("gradle-plugin", libs.versions.kotlin.get()))
|
||||
implementation(libs.android.gradle.plugin)
|
||||
implementation(libs.ksp.plugin)
|
||||
implementation(libs.navigation.safe.args.plugin)
|
||||
implementation(libs.lsparanoid.plugin)
|
||||
implementation(libs.moshi.plugin)
|
||||
implementation(libs.jgit)
|
||||
}
|
||||
77
app/buildSrc/src/main/java/AddCommentTask.kt
Normal file
77
app/buildSrc/src/main/java/AddCommentTask.kt
Normal file
@@ -0,0 +1,77 @@
|
||||
import com.android.build.api.artifact.ArtifactTransformationRequest
|
||||
import com.android.build.api.dsl.ApkSigningConfig
|
||||
import com.android.builder.internal.packaging.IncrementalPackager
|
||||
import com.android.tools.build.apkzlib.sign.SigningExtension
|
||||
import com.android.tools.build.apkzlib.sign.SigningOptions
|
||||
import com.android.tools.build.apkzlib.zfile.ZFiles
|
||||
import com.android.tools.build.apkzlib.zip.ZFileOptions
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import java.io.File
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.jar.JarFile
|
||||
|
||||
abstract class AddCommentTask: DefaultTask() {
|
||||
@get:Input
|
||||
abstract val comment: Property<String>
|
||||
|
||||
@get:Input
|
||||
abstract val signingConfig: Property<ApkSigningConfig>
|
||||
|
||||
@get:InputFiles
|
||||
abstract val apkFolder: DirectoryProperty
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outFolder: DirectoryProperty
|
||||
|
||||
@get:Internal
|
||||
abstract val transformationRequest: Property<ArtifactTransformationRequest<AddCommentTask>>
|
||||
|
||||
@TaskAction
|
||||
fun taskAction() = transformationRequest.get().submit(this) { artifact ->
|
||||
val inFile = File(artifact.outputFile)
|
||||
val outFile = outFolder.file(inFile.name).get().asFile
|
||||
|
||||
val privateKey = signingConfig.get().getPrivateKey()
|
||||
val signingOptions = SigningOptions.builder()
|
||||
.setMinSdkVersion(0)
|
||||
.setV1SigningEnabled(true)
|
||||
.setV2SigningEnabled(true)
|
||||
.setKey(privateKey.privateKey)
|
||||
.setCertificates(privateKey.certificate as X509Certificate)
|
||||
.setValidation(SigningOptions.Validation.ASSUME_INVALID)
|
||||
.build()
|
||||
val options = ZFileOptions().apply {
|
||||
noTimestamps = true
|
||||
autoSortFiles = true
|
||||
}
|
||||
outFile.parentFile?.mkdirs()
|
||||
inFile.copyTo(outFile, overwrite = true)
|
||||
ZFiles.apk(outFile, options).use {
|
||||
SigningExtension(signingOptions).register(it)
|
||||
it.eocdComment = comment.get().toByteArray()
|
||||
it.get(IncrementalPackager.APP_METADATA_ENTRY_PATH)?.delete()
|
||||
it.get(IncrementalPackager.VERSION_CONTROL_INFO_ENTRY_PATH)?.delete()
|
||||
it.get(JarFile.MANIFEST_NAME)?.delete()
|
||||
}
|
||||
|
||||
outFile
|
||||
}
|
||||
|
||||
private fun ApkSigningConfig.getPrivateKey(): KeyStore.PrivateKeyEntry {
|
||||
val keyStore = KeyStore.getInstance(storeType ?: KeyStore.getDefaultType())
|
||||
storeFile!!.inputStream().use {
|
||||
keyStore.load(it, storePassword!!.toCharArray())
|
||||
}
|
||||
val keyPwdArray = keyPassword!!.toCharArray()
|
||||
val entry = keyStore.getEntry(keyAlias!!, KeyStore.PasswordProtection(keyPwdArray))
|
||||
return entry as KeyStore.PrivateKeyEntry
|
||||
}
|
||||
}
|
||||
122
app/buildSrc/src/main/java/DesugarClassVisitorFactory.kt
Normal file
122
app/buildSrc/src/main/java/DesugarClassVisitorFactory.kt
Normal file
@@ -0,0 +1,122 @@
|
||||
import com.android.build.api.instrumentation.AsmClassVisitorFactory
|
||||
import com.android.build.api.instrumentation.ClassContext
|
||||
import com.android.build.api.instrumentation.ClassData
|
||||
import com.android.build.api.instrumentation.InstrumentationParameters
|
||||
import org.objectweb.asm.ClassVisitor
|
||||
import org.objectweb.asm.MethodVisitor
|
||||
import org.objectweb.asm.Opcodes
|
||||
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_OUT_STREAM_CLASS_NAME = "org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream"
|
||||
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 DESUGAR_GET_TIME_DESC =
|
||||
"(Ljava/util/zip/ZipEntry;)Ljava/nio/file/attribute/FileTime;"
|
||||
|
||||
private fun ClassData.isTypeOf(name: String) = className == name || superClasses.contains(name)
|
||||
|
||||
abstract class DesugarClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {
|
||||
override fun createClassVisitor(
|
||||
classContext: ClassContext,
|
||||
nextClassVisitor: ClassVisitor
|
||||
): ClassVisitor {
|
||||
return if (classContext.currentClassData.className == ZIP_OUT_STREAM_CLASS_NAME) {
|
||||
ZipEntryPatcher(classContext, ZipOutputStreamPatcher(nextClassVisitor))
|
||||
} else {
|
||||
ZipEntryPatcher(classContext, nextClassVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isInstrumentable(classData: ClassData) = classData.className != DESUGAR_CLASS_NAME
|
||||
|
||||
// Patch ALL references to ZipEntry#getXXXTime
|
||||
class ZipEntryPatcher(
|
||||
private val classContext: ClassContext,
|
||||
cv: ClassVisitor
|
||||
) : ClassVisitor(ASM9, cv) {
|
||||
override fun visitMethod(
|
||||
access: Int,
|
||||
name: String?,
|
||||
descriptor: String?,
|
||||
signature: String?,
|
||||
exceptions: Array<out String>?
|
||||
) = MethodPatcher(super.visitMethod(access, name, descriptor, signature, exceptions))
|
||||
|
||||
inner class MethodPatcher(mv: MethodVisitor?) : MethodVisitor(ASM9, mv) {
|
||||
override fun visitMethodInsn(
|
||||
opcode: Int,
|
||||
owner: String,
|
||||
name: String,
|
||||
descriptor: String,
|
||||
isInterface: Boolean
|
||||
) {
|
||||
if (!process(owner, name, descriptor)) {
|
||||
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
|
||||
}
|
||||
}
|
||||
|
||||
private fun process(owner: String, name: String, descriptor: String): Boolean {
|
||||
val classData = classContext.loadClassData(owner.replace("/", ".")) ?: return false
|
||||
if (!classData.isTypeOf(ZIP_ENTRY_CLASS_NAME))
|
||||
return false
|
||||
if (descriptor != ZIP_ENTRY_GET_TIME_DESC)
|
||||
return false
|
||||
return when (name) {
|
||||
"getLastModifiedTime", "getLastAccessTime", "getCreationTime" -> {
|
||||
mv.visitMethodInsn(
|
||||
Opcodes.INVOKESTATIC,
|
||||
DESUGAR_CLASS_NAME.replace('.', '/'),
|
||||
name,
|
||||
DESUGAR_GET_TIME_DESC,
|
||||
false
|
||||
)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,12 @@ import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.provideDelegate
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
import java.util.Random
|
||||
|
||||
// Set non-zero value here to fix the random seed for reproducible builds
|
||||
// CI builds are always reproducible
|
||||
val RAND_SEED = if (System.getenv("CI") != null) 42 else 0
|
||||
lateinit var RANDOM: Random
|
||||
|
||||
private val props = Properties()
|
||||
private var commitHash = ""
|
||||
@@ -14,7 +20,7 @@ private val defaultAbis = setOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
|
||||
object Config {
|
||||
operator fun get(key: String): String? {
|
||||
val v = props[key] as? String ?: return null
|
||||
return if (v.isBlank()) null else v
|
||||
return v.ifBlank { null }
|
||||
}
|
||||
|
||||
fun contains(key: String) = get(key) != null
|
||||
@@ -28,19 +34,25 @@ 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> {
|
||||
override fun apply(project: Project) = project.applyPlugin()
|
||||
|
||||
private fun Project.applyPlugin() {
|
||||
initRandom(rootProject.file("app/dict.txt"))
|
||||
initRandom(rootProject.file("dict.txt"))
|
||||
props.clear()
|
||||
rootProject.file("gradle.properties").inputStream().use { props.load(it) }
|
||||
val configPath: String? by this
|
||||
val config = configPath?.let { File(it) } ?: rootProject.file("config.prop")
|
||||
val config = rootFile(configPath ?: "config.prop")
|
||||
if (config.exists())
|
||||
config.inputStream().use { props.load(it) }
|
||||
|
||||
val repo = FileRepository(rootProject.file(".git"))
|
||||
val repo = FileRepository(rootFile(".git"))
|
||||
val refId = repo.refDatabase.exactRef("HEAD").objectId
|
||||
commitHash = repo.newObjectReader().abbreviate(refId, 8).name()
|
||||
}
|
||||
330
app/buildSrc/src/main/java/Setup.kt
Normal file
330
app/buildSrc/src/main/java/Setup.kt
Normal file
@@ -0,0 +1,330 @@
|
||||
import com.android.build.api.artifact.SingleArtifact
|
||||
import com.android.build.api.instrumentation.FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
|
||||
import com.android.build.api.instrumentation.InstrumentationScope
|
||||
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
||||
import com.android.build.gradle.BaseExtension
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import org.apache.tools.ant.filters.FixCrLfFilter
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.tasks.Copy
|
||||
import org.gradle.api.tasks.Delete
|
||||
import org.gradle.api.tasks.StopExecutionException
|
||||
import org.gradle.api.tasks.Sync
|
||||
import org.gradle.kotlin.dsl.assign
|
||||
import org.gradle.kotlin.dsl.exclude
|
||||
import org.gradle.kotlin.dsl.filter
|
||||
import org.gradle.kotlin.dsl.get
|
||||
import org.gradle.kotlin.dsl.getValue
|
||||
import org.gradle.kotlin.dsl.named
|
||||
import org.gradle.kotlin.dsl.provideDelegate
|
||||
import org.gradle.kotlin.dsl.register
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.security.MessageDigest
|
||||
import java.util.HexFormat
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
private fun Project.androidBase(configure: Action<BaseExtension>) =
|
||||
extensions.configure("android", configure)
|
||||
|
||||
private fun Project.android(configure: Action<BaseAppModuleExtension>) =
|
||||
extensions.configure("android", configure)
|
||||
|
||||
internal val Project.androidApp: BaseAppModuleExtension
|
||||
get() = extensions["android"] as BaseAppModuleExtension
|
||||
|
||||
private val Project.androidLib: LibraryExtension
|
||||
get() = extensions["android"] as LibraryExtension
|
||||
|
||||
internal val Project.androidComponents
|
||||
get() = extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
|
||||
|
||||
fun Project.setupCommon() {
|
||||
androidBase {
|
||||
compileSdkVersion(36)
|
||||
buildToolsVersion = "36.0.0"
|
||||
ndkPath = "$sdkDirectory/ndk/magisk"
|
||||
ndkVersion = "29.0.13846066"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += arrayOf(
|
||||
"/META-INF/*",
|
||||
"/META-INF/androidx/**",
|
||||
"/META-INF/versions/**",
|
||||
"/org/bouncycastle/**",
|
||||
"/org/apache/commons/**",
|
||||
"/kotlin/**",
|
||||
"/kotlinx/**",
|
||||
"/okhttp3/**",
|
||||
"/*.txt",
|
||||
"/*.bin",
|
||||
"/*.json",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7")
|
||||
exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_21
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 =
|
||||
"https://github.com/topjohnwu/magisk-files/releases/download/files/busybox-1.36.1.1.zip"
|
||||
const val BUSYBOX_ZIP_CHECKSUM =
|
||||
"b4d0551feabaf314e53c79316c980e8f66432e9fb91a69dbbf10a93564b40951"
|
||||
|
||||
fun Project.setupCoreLib() {
|
||||
setupCommon()
|
||||
|
||||
androidLib.libraryVariants.all {
|
||||
val variant = name
|
||||
val variantCapped = name.replaceFirstChar { it.uppercase() }
|
||||
val abiList = Config.abiList
|
||||
|
||||
val syncLibs = tasks.register("sync${variantCapped}JniLibs", Sync::class) {
|
||||
into("src/$variant/jniLibs")
|
||||
for (abi in abiList) {
|
||||
into(abi) {
|
||||
from(rootFile("native/out/$abi")) {
|
||||
include("magiskboot", "magiskinit", "magiskpolicy", "magisk", "libinit-ld.so")
|
||||
rename { if (it.endsWith(".so")) it else "lib$it.so" }
|
||||
}
|
||||
}
|
||||
}
|
||||
from(zipTree(downloadFile(BUSYBOX_DOWNLOAD_URL, BUSYBOX_ZIP_CHECKSUM)))
|
||||
include(abiList.map { "$it/libbusybox.so" })
|
||||
onlyIf {
|
||||
if (inputs.sourceFiles.files.size != abiList.size * 6)
|
||||
throw StopExecutionException("Please build binaries first! (./build.py binary)")
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
tasks.getByPath("merge${variantCapped}JniLibFolders").dependsOn(syncLibs)
|
||||
|
||||
val syncResources = tasks.register("sync${variantCapped}Resources", Sync::class) {
|
||||
into("src/$variant/resources/META-INF/com/google/android")
|
||||
from(rootFile("scripts/update_binary.sh")) {
|
||||
rename { "update-binary" }
|
||||
}
|
||||
from(rootFile("scripts/flash_script.sh")) {
|
||||
rename { "updater-script" }
|
||||
}
|
||||
}
|
||||
|
||||
processJavaResourcesProvider.configure { dependsOn(syncResources) }
|
||||
|
||||
val stubTask = tasks.getByPath(":stub:comment$variantCapped")
|
||||
val stubApk = stubTask.outputs.files.asFileTree.filter {
|
||||
it.name.endsWith(".apk")
|
||||
}
|
||||
|
||||
val syncAssets = tasks.register("sync${variantCapped}Assets", Sync::class) {
|
||||
dependsOn(stubTask)
|
||||
inputs.property("version", Config.version)
|
||||
inputs.property("versionCode", Config.versionCode)
|
||||
into("src/$variant/assets")
|
||||
from(rootFile("scripts")) {
|
||||
include("util_functions.sh", "boot_patch.sh", "addon.d.sh",
|
||||
"app_functions.sh", "uninstaller.sh", "module_installer.sh")
|
||||
}
|
||||
from(rootFile("tools/bootctl"))
|
||||
into("chromeos") {
|
||||
from(rootFile("tools/futility"))
|
||||
from(rootFile("tools/keys")) {
|
||||
include("kernel_data_key.vbprivk", "kernel.keyblock")
|
||||
}
|
||||
}
|
||||
from(stubApk) {
|
||||
rename { "stub.apk" }
|
||||
}
|
||||
filesMatching("**/util_functions.sh") {
|
||||
filter {
|
||||
it.replace(
|
||||
"#MAGISK_VERSION_STUB",
|
||||
"MAGISK_VER='${Config.version}'\nMAGISK_VER_CODE=${Config.versionCode}"
|
||||
)
|
||||
}
|
||||
filter<FixCrLfFilter>("eol" to FixCrLfFilter.CrLf.newInstance("lf"))
|
||||
}
|
||||
}
|
||||
mergeAssetsProvider.configure { dependsOn(syncAssets) }
|
||||
}
|
||||
|
||||
tasks.named<Delete>("clean") {
|
||||
delete.addAll(listOf("src/main/jniLibs", "src/main/resources", "src/debug", "src/release"))
|
||||
}
|
||||
}
|
||||
|
||||
fun Project.setupAppCommon() {
|
||||
setupCommon()
|
||||
|
||||
android {
|
||||
signingConfigs {
|
||||
Config["keyStore"]?.also {
|
||||
create("config") {
|
||||
storeFile = rootFile(it)
|
||||
storePassword = Config["keyStorePass"]
|
||||
keyAlias = Config["keyAlias"]
|
||||
keyPassword = Config["keyPass"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
targetSdk = 36
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt")
|
||||
)
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
val config = signingConfigs.findByName("config") ?: signingConfigs["debug"]
|
||||
debug {
|
||||
signingConfig = config
|
||||
}
|
||||
release {
|
||||
signingConfig = config
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
disable += "MissingTranslation"
|
||||
checkReleaseBuilds = false
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents.onVariants { variant ->
|
||||
val commentTask = tasks.register(
|
||||
"comment${variant.name.replaceFirstChar { it.uppercase() }}",
|
||||
AddCommentTask::class.java
|
||||
)
|
||||
val transformationRequest = variant.artifacts.use(commentTask)
|
||||
.wiredWithDirectories(AddCommentTask::apkFolder, AddCommentTask::outFolder)
|
||||
.toTransformMany(SingleArtifact.APK)
|
||||
val signingConfig = androidApp.buildTypes.getByName(variant.buildType!!).signingConfig
|
||||
commentTask.configure {
|
||||
this.transformationRequest = transformationRequest
|
||||
this.signingConfig = signingConfig
|
||||
this.comment = "version=${Config.version}\n" +
|
||||
"versionCode=${Config.versionCode}\n" +
|
||||
"stubVersion=${Config.stubVersion}\n"
|
||||
this.outFolder.set(layout.buildDirectory.dir("outputs/apk/${variant.name}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Project.setupMainApk() {
|
||||
setupAppCommon()
|
||||
|
||||
android {
|
||||
namespace = "com.topjohnwu.magisk"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.topjohnwu.magisk"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
versionName = Config.version
|
||||
versionCode = Config.versionCode
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64", "riscv64")
|
||||
debugSymbolLevel = "FULL"
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents.onVariants { variant ->
|
||||
variant.instrumentation.apply {
|
||||
setAsmFramesComputationMode(COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)
|
||||
transformClassesWith(
|
||||
DesugarClassVisitorFactory::class.java, InstrumentationScope.ALL) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import com.android.build.api.artifact.SingleArtifact
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.CacheableTask
|
||||
import org.gradle.api.tasks.Delete
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.InputFile
|
||||
import org.gradle.api.tasks.InputFiles
|
||||
@@ -10,22 +13,25 @@ import org.gradle.api.tasks.OutputFile
|
||||
import org.gradle.api.tasks.PathSensitive
|
||||
import org.gradle.api.tasks.PathSensitivity
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.kotlin.dsl.assign
|
||||
import org.gradle.kotlin.dsl.named
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.PrintStream
|
||||
import java.security.SecureRandom
|
||||
import java.util.Random
|
||||
import java.util.zip.Deflater
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipOutputStream
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherOutputStream
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.random.asKotlinRandom
|
||||
|
||||
// Set non-zero value here to fix the random seed for reproducible builds
|
||||
// CI builds are always reproducible
|
||||
val RAND_SEED = if (System.getenv("CI") != null) 42 else 0
|
||||
private lateinit var RANDOM: Random
|
||||
private val kRANDOM get() = RANDOM.asKotlinRandom()
|
||||
|
||||
private val c1 = mutableListOf<String>()
|
||||
@@ -72,7 +78,7 @@ private fun PrintStream.byteField(name: String, bytes: ByteArray) {
|
||||
}
|
||||
|
||||
@CacheableTask
|
||||
abstract class ManifestUpdater: DefaultTask() {
|
||||
private abstract class ManifestUpdater: DefaultTask() {
|
||||
@get:Input
|
||||
abstract val applicationId: Property<String>
|
||||
|
||||
@@ -182,9 +188,7 @@ abstract class ManifestUpdater: DefaultTask() {
|
||||
}
|
||||
|
||||
|
||||
fun genStubClasses(factoryOutDir: File, appOutDir: File) {
|
||||
fun String.ind(level: Int) = replaceIndentByMargin(" ".repeat(level))
|
||||
|
||||
private fun genStubClasses(factoryOutDir: File, appOutDir: File) {
|
||||
val classNameGenerator = sequence {
|
||||
fun notJavaKeyword(name: String) = when (name) {
|
||||
"do", "if", "for", "int", "new", "try" -> false
|
||||
@@ -228,7 +232,7 @@ fun genStubClasses(factoryOutDir: File, appOutDir: File) {
|
||||
genClass("StubApplication", appOutDir)
|
||||
}
|
||||
|
||||
fun genEncryptedResources(res: ByteArray, outDir: File) {
|
||||
private fun genEncryptedResources(res: ByteArray, outDir: File) {
|
||||
val mainPkgDir = File(outDir, "com/topjohnwu/magisk")
|
||||
mainPkgDir.mkdirs()
|
||||
|
||||
@@ -259,3 +263,86 @@ fun genEncryptedResources(res: ByteArray, outDir: File) {
|
||||
it.println("}")
|
||||
}
|
||||
}
|
||||
|
||||
fun Project.setupStubApk() {
|
||||
setupAppCommon()
|
||||
|
||||
androidComponents.onVariants { variant ->
|
||||
val variantName = variant.name
|
||||
val variantCapped = variantName.replaceFirstChar { it.uppercase() }
|
||||
val manifestUpdater =
|
||||
project.tasks.register("${variantName}ManifestProducer", ManifestUpdater::class.java) {
|
||||
dependsOn("generate${variantCapped}ObfuscatedClass")
|
||||
applicationId = variant.applicationId
|
||||
appClassDir.set(layout.buildDirectory.dir("generated/source/app/$variantName"))
|
||||
factoryClassDir.set(layout.buildDirectory.dir("generated/source/factory/$variantName"))
|
||||
}
|
||||
variant.artifacts.use(manifestUpdater)
|
||||
.wiredWithFiles(
|
||||
ManifestUpdater::mergedManifest,
|
||||
ManifestUpdater::outputManifest)
|
||||
.toTransform(SingleArtifact.MERGED_MANIFEST)
|
||||
}
|
||||
|
||||
androidApp.applicationVariants.all {
|
||||
val variantCapped = name.replaceFirstChar { it.uppercase() }
|
||||
val variantLowered = name.lowercase()
|
||||
val outFactoryClassDir = layout.buildDirectory.file("generated/source/factory/${variantLowered}").get().asFile
|
||||
val outAppClassDir = layout.buildDirectory.file("generated/source/app/${variantLowered}").get().asFile
|
||||
val outResDir = layout.buildDirectory.dir("generated/source/res/${variantLowered}").get().asFile
|
||||
val aapt = File(androidApp.sdkDirectory, "build-tools/${androidApp.buildToolsVersion}/aapt2")
|
||||
val apk = layout.buildDirectory.file("intermediates/linked_resources_binary_format/" +
|
||||
"${variantLowered}/process${variantCapped}Resources/linked-resources-binary-format-${variantLowered}.ap_").get().asFile
|
||||
|
||||
val genManifestTask = tasks.register("generate${variantCapped}ObfuscatedClass") {
|
||||
inputs.property("seed", RAND_SEED)
|
||||
outputs.dirs(outFactoryClassDir, outAppClassDir)
|
||||
doLast {
|
||||
outFactoryClassDir.mkdirs()
|
||||
outAppClassDir.mkdirs()
|
||||
genStubClasses(outFactoryClassDir, outAppClassDir)
|
||||
}
|
||||
}
|
||||
registerJavaGeneratingTask(genManifestTask, outFactoryClassDir, outAppClassDir)
|
||||
|
||||
val processResourcesTask = tasks.named("process${variantCapped}Resources") {
|
||||
outputs.dir(outResDir)
|
||||
doLast {
|
||||
val apkTmp = File("${apk}.tmp")
|
||||
providers.exec {
|
||||
commandLine(aapt, "optimize", "-o", apkTmp, "--collapse-resource-names", apk)
|
||||
}.result.get()
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
ZipFile(apkTmp).use { src ->
|
||||
ZipOutputStream(apk.outputStream()).use {
|
||||
it.setLevel(Deflater.BEST_COMPRESSION)
|
||||
it.putNextEntry(ZipEntry("AndroidManifest.xml"))
|
||||
src.getInputStream(src.getEntry("AndroidManifest.xml")).transferTo(it)
|
||||
it.closeEntry()
|
||||
}
|
||||
DeflaterOutputStream(bos, Deflater(Deflater.BEST_COMPRESSION)).use {
|
||||
src.getInputStream(src.getEntry("resources.arsc")).transferTo(it)
|
||||
}
|
||||
}
|
||||
apkTmp.delete()
|
||||
genEncryptedResources(bos.toByteArray(), outResDir)
|
||||
}
|
||||
}
|
||||
|
||||
registerJavaGeneratingTask(processResourcesTask, outResDir)
|
||||
}
|
||||
// Override optimizeReleaseResources task
|
||||
val apk = layout.buildDirectory.file("intermediates/linked_resources_binary_format/" +
|
||||
"release/processReleaseResources/linked-resources-binary-format-release.ap_").get().asFile
|
||||
val optRes = layout.buildDirectory.file("intermediates/optimized_processed_res/" +
|
||||
"release/optimizeReleaseResources/resources-release-optimize.ap_").get().asFile
|
||||
afterEvaluate {
|
||||
tasks.named("optimizeReleaseResources") {
|
||||
doLast { apk.copyTo(optRes, true) }
|
||||
}
|
||||
}
|
||||
tasks.named<Delete>("clean") {
|
||||
delete.addAll(listOf("src/debug/AndroidManifest.xml", "src/release/AndroidManifest.xml"))
|
||||
}
|
||||
}
|
||||
5
app/core/.gitignore
vendored
5
app/core/.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
/build
|
||||
src/*/assets
|
||||
src/*/jniLibs
|
||||
src/*/resources
|
||||
src/debug
|
||||
src/release
|
||||
|
||||
@@ -2,6 +2,7 @@ plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
kotlin("plugin.parcelize")
|
||||
id("dev.zacsweers.moshix")
|
||||
id("com.google.devtools.ksp")
|
||||
}
|
||||
|
||||
@@ -19,16 +20,22 @@ android {
|
||||
buildConfigField("int", "APP_VERSION_CODE", "${Config.versionCode}")
|
||||
buildConfigField("String", "APP_VERSION_NAME", "\"${Config.version}\"")
|
||||
buildConfigField("int", "STUB_VERSION", Config.stubVersion)
|
||||
consumerProguardFile("proguard-rules.pro")
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":app:shared"))
|
||||
api(project(":shared"))
|
||||
coreLibraryDesugaring(libs.jdk.libs)
|
||||
|
||||
api(libs.timber)
|
||||
api(libs.markwon.core)
|
||||
@@ -47,9 +54,6 @@ dependencies {
|
||||
implementation(libs.okhttp.logging)
|
||||
implementation(libs.okhttp.dnsoverhttps)
|
||||
|
||||
implementation(libs.moshi)
|
||||
ksp(libs.moshi.codegen)
|
||||
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.ktx)
|
||||
ksp(libs.room.compiler)
|
||||
@@ -59,5 +63,10 @@ dependencies {
|
||||
implementation(libs.activity)
|
||||
implementation(libs.collection.ktx)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -22,42 +22,20 @@
|
||||
int mActivityHandlesConfigFlags;
|
||||
}
|
||||
|
||||
# main
|
||||
-keep,allowoptimization public class com.topjohnwu.magisk.signing.SignBoot {
|
||||
public static void main(java.lang.String[]);
|
||||
}
|
||||
|
||||
# Strip Timber verbose and debug logging
|
||||
-assumenosideeffects class timber.log.Timber$Tree {
|
||||
public void v(**);
|
||||
public void d(**);
|
||||
}
|
||||
|
||||
# https://github.com/square/retrofit/issues/3751#issuecomment-1192043644
|
||||
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
|
||||
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||
|
||||
# With R8 full mode generic signatures are stripped for classes that are not
|
||||
# kept. Suspend functions are wrapped in continuations where the type argument
|
||||
# is used.
|
||||
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
||||
|
||||
|
||||
# Excessive obfuscation
|
||||
-repackageclasses 'a'
|
||||
-flattenpackagehierarchy
|
||||
-allowaccessmodification
|
||||
|
||||
-obfuscationdictionary ../dict.txt
|
||||
-classobfuscationdictionary ../dict.txt
|
||||
-packageobfuscationdictionary ../dict.txt
|
||||
|
||||
-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
|
||||
-dontwarn org.junit.**
|
||||
-dontwarn org.apache.**
|
||||
@@ -14,7 +14,6 @@
|
||||
<application
|
||||
android:name=".App"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:multiArch="true"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||
tools:remove="android:appComponentFactory">
|
||||
|
||||
|
||||
@@ -6,4 +6,5 @@ package com.topjohnwu.magisk.core.utils;
|
||||
interface IRootUtils {
|
||||
android.app.ActivityManager.RunningAppProcessInfo getAppProcess(int pid);
|
||||
IBinder getFileSystem();
|
||||
boolean addSystemlessHosts();
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.base.UntrackedActivity
|
||||
import com.topjohnwu.magisk.core.utils.LocaleSetting
|
||||
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.ShellInit
|
||||
import com.topjohnwu.superuser.Shell
|
||||
@@ -40,6 +39,7 @@ object AppContext : ContextWrapper(null),
|
||||
|
||||
private var ref = WeakReference<Activity>(null)
|
||||
private lateinit var application: Application
|
||||
private lateinit var networkObserver: NetworkObserver
|
||||
|
||||
init {
|
||||
// Always log full stack trace with Timber
|
||||
@@ -56,6 +56,10 @@ object AppContext : ContextWrapper(null),
|
||||
LocaleSetting.instance.updateResource(resources)
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
networkObserver.postCurrentState()
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
if (activity is UntrackedActivity) return
|
||||
ref = WeakReference(activity)
|
||||
@@ -102,8 +106,7 @@ object AppContext : ContextWrapper(null),
|
||||
val lm = getSystemService(LocaleManager::class.java)
|
||||
lm.overrideLocaleConfig = LocaleSetting.localeConfig
|
||||
}
|
||||
ProcessLifecycle.init(this)
|
||||
NetworkObserver.init(this)
|
||||
networkObserver = NetworkObserver.init(this)
|
||||
if (!BuildConfig.DEBUG && !isRunningAsStub) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ProfileInstaller.writeProfile(this@AppContext)
|
||||
@@ -120,7 +123,6 @@ object AppContext : ContextWrapper(null),
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
|
||||
@@ -32,8 +32,9 @@ object Config : PreferenceConfig, DBConfig {
|
||||
const val SU_NOTIFICATION = "su_notification"
|
||||
const val SU_REAUTH = "su_reauth"
|
||||
const val SU_TAPJACK = "su_tapjack"
|
||||
const val SU_RESTRICT = "su_restrict"
|
||||
const val CHECK_UPDATES = "check_update"
|
||||
const val UPDATE_CHANNEL = "update_channel"
|
||||
const val RELEASE_CHANNEL = "release_channel"
|
||||
const val CUSTOM_CHANNEL = "custom_channel"
|
||||
const val LOCALE = "locale"
|
||||
const val DARK_THEME = "dark_theme_extended"
|
||||
@@ -48,7 +49,7 @@ object Config : PreferenceConfig, DBConfig {
|
||||
SU_AUTO_RESPONSE, SU_REAUTH, SU_TAPJACK)
|
||||
}
|
||||
|
||||
object Value {
|
||||
object OldValue {
|
||||
// Update channels
|
||||
const val DEFAULT_CHANNEL = -1
|
||||
const val STABLE_CHANNEL = 0
|
||||
@@ -56,6 +57,15 @@ object Config : PreferenceConfig, DBConfig {
|
||||
const val CUSTOM_CHANNEL = 2
|
||||
const val CANARY_CHANNEL = 3
|
||||
const val DEBUG_CHANNEL = 4
|
||||
}
|
||||
|
||||
object Value {
|
||||
// Update channels
|
||||
const val DEFAULT_CHANNEL = -1
|
||||
const val STABLE_CHANNEL = 0
|
||||
const val BETA_CHANNEL = 1
|
||||
const val DEBUG_CHANNEL = 2
|
||||
const val CUSTOM_CHANNEL = 3
|
||||
|
||||
// root access mode
|
||||
const val ROOT_ACCESS_DISABLED = 0
|
||||
@@ -83,17 +93,9 @@ object Config : PreferenceConfig, DBConfig {
|
||||
const val SU_AUTO_ALLOW = 2
|
||||
|
||||
// su timeout
|
||||
val TIMEOUT_LIST = intArrayOf(0, -1, 10, 20, 30, 60)
|
||||
val TIMEOUT_LIST = longArrayOf(0, -1, 10, 20, 30, 60)
|
||||
}
|
||||
|
||||
private val defaultChannel =
|
||||
if (BuildConfig.DEBUG)
|
||||
Value.DEBUG_CHANNEL
|
||||
else if (Const.APP_IS_CANARY)
|
||||
Value.CANARY_CHANNEL
|
||||
else
|
||||
Value.DEFAULT_CHANNEL
|
||||
|
||||
@JvmField var keepVerity = false
|
||||
@JvmField var keepEnc = false
|
||||
@JvmField var recovery = false
|
||||
@@ -109,7 +111,7 @@ object Config : PreferenceConfig, DBConfig {
|
||||
private var checkUpdatePrefs by preference(Key.CHECK_UPDATES, true)
|
||||
private var localePrefs by preference(Key.LOCALE, "")
|
||||
var doh by preference(Key.DOH, false)
|
||||
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
|
||||
var updateChannel by preference(Key.RELEASE_CHANNEL, Value.DEFAULT_CHANNEL)
|
||||
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
||||
var downloadDir by preference(Key.DOWNLOAD_DIR, "")
|
||||
var randName by preference(Key.RAND_NAME, true)
|
||||
@@ -146,8 +148,10 @@ object Config : PreferenceConfig, DBConfig {
|
||||
}
|
||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||
var suTapjack by preference(Key.SU_TAPJACK, true)
|
||||
var suRestrict by preference(Key.SU_RESTRICT, false)
|
||||
|
||||
private const val SU_FINGERPRINT = "su_fingerprint"
|
||||
private const val UPDATE_CHANNEL = "update_channel"
|
||||
|
||||
fun toBundle(): Bundle {
|
||||
val map = prefs.all - Key.NO_MIGRATION
|
||||
@@ -183,17 +187,23 @@ object Config : PreferenceConfig, DBConfig {
|
||||
}
|
||||
|
||||
prefs.edit {
|
||||
// Settings migration
|
||||
// Migrate su_fingerprint
|
||||
if (prefs.getBoolean(SU_FINGERPRINT, false))
|
||||
suBiometric = true
|
||||
remove(SU_FINGERPRINT)
|
||||
prefs.getString(Key.UPDATE_CHANNEL, null).also {
|
||||
if (it == null ||
|
||||
it.toInt() > Value.DEBUG_CHANNEL ||
|
||||
it.toInt() < Value.DEFAULT_CHANNEL) {
|
||||
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate update_channel
|
||||
prefs.getString(UPDATE_CHANNEL, null)?.let {
|
||||
val channel = when (it.toInt()) {
|
||||
OldValue.STABLE_CHANNEL -> Value.STABLE_CHANNEL
|
||||
OldValue.CANARY_CHANNEL, OldValue.BETA_CHANNEL -> Value.BETA_CHANNEL
|
||||
OldValue.DEBUG_CHANNEL -> Value.DEBUG_CHANNEL
|
||||
OldValue.CUSTOM_CHANNEL -> Value.CUSTOM_CHANNEL
|
||||
else -> Value.DEFAULT_CHANNEL
|
||||
}
|
||||
putInt(Key.RELEASE_CHANNEL, channel)
|
||||
}
|
||||
remove(UPDATE_CHANNEL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.topjohnwu.magisk.core
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
object Const {
|
||||
@@ -14,23 +15,22 @@ object Const {
|
||||
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
|
||||
|
||||
// Paths
|
||||
const val MAGISK_PATH = "/data/adb/modules"
|
||||
const val MODULE_PATH = "/data/adb/modules"
|
||||
const val TMPDIR = "/dev/tmp"
|
||||
const val MAGISK_LOG = "/cache/magisk.log"
|
||||
|
||||
// Misc
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.APP_VERSION_CODE)
|
||||
|
||||
object Version {
|
||||
const val MIN_VERSION = "v22.0"
|
||||
const val MIN_VERCODE = 22000
|
||||
|
||||
private fun isCanary() = (Info.env.versionCode % 100) != 0
|
||||
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
|
||||
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
|
||||
fun isCanary() = isCanary(Info.env.versionCode)
|
||||
|
||||
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
||||
fun atLeast_28_0() = Info.env.versionCode >= 28000 || isCanary()
|
||||
fun atLeast_30_1() = Info.env.versionCode >= 30100 || isCanary()
|
||||
}
|
||||
|
||||
object ID {
|
||||
@@ -42,13 +42,9 @@ object Const {
|
||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||
|
||||
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_PAGE_URL = "https://topjohnwu.github.io/magisk-files/"
|
||||
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
|
||||
const val INVALID_URL = "https://example.com/"
|
||||
}
|
||||
|
||||
object Key {
|
||||
|
||||
@@ -19,12 +19,18 @@ object Info {
|
||||
|
||||
var stub: StubApk.Data? = null
|
||||
|
||||
val EMPTY_REMOTE = UpdateInfo()
|
||||
var remote = EMPTY_REMOTE
|
||||
suspend fun getRemote(svc: NetworkService): UpdateInfo? {
|
||||
return if (remote === EMPTY_REMOTE) {
|
||||
svc.fetchUpdate()?.apply { remote = this }
|
||||
} else remote
|
||||
private val EMPTY_UPDATE = UpdateInfo()
|
||||
var update = EMPTY_UPDATE
|
||||
private set
|
||||
|
||||
suspend fun fetchUpdate(svc: NetworkService): UpdateInfo? {
|
||||
return if (update === EMPTY_UPDATE) {
|
||||
svc.fetchUpdate()?.apply { update = this }
|
||||
} else update
|
||||
}
|
||||
|
||||
fun resetUpdate() {
|
||||
update = EMPTY_UPDATE
|
||||
}
|
||||
|
||||
var isRooted = false
|
||||
@@ -41,13 +47,14 @@ object Info {
|
||||
private set
|
||||
var slot = ""
|
||||
private set
|
||||
var isVendorBoot = false
|
||||
private set
|
||||
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
||||
@JvmStatic val isFDE get() = crypto == "block"
|
||||
@JvmStatic var ramdisk = false
|
||||
private set
|
||||
private var crypto = ""
|
||||
|
||||
var hasGMS = true
|
||||
val isEmulator =
|
||||
Build.DEVICE.contains("vsoc")
|
||||
|| getProperty("ro.kernel.qemu", "0") == "1"
|
||||
@@ -108,6 +115,7 @@ object Info {
|
||||
crypto = getVar("CRYPTOTYPE")
|
||||
slot = getVar("SLOT")
|
||||
legacySAR = getBool("LEGACYSAR")
|
||||
isVendorBoot = getBool("VENDORBOOT")
|
||||
|
||||
// Default presets
|
||||
Config.recovery = getBool("RECOVERYMODE")
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.core.base.BaseJobService
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
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.view.Notifications
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -25,7 +26,7 @@ class JobService : BaseJobService() {
|
||||
@TargetApi(value = 34)
|
||||
inner class Session(
|
||||
private var params: JobParameters
|
||||
) : DownloadEngine.Session {
|
||||
) : DownloadSession {
|
||||
|
||||
override val context get() = this@JobService
|
||||
val engine = DownloadEngine(this)
|
||||
@@ -74,9 +75,8 @@ class JobService : BaseJobService() {
|
||||
|
||||
private fun checkUpdate(params: JobParameters): Boolean {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ServiceLocator.networkService.fetchUpdate()?.let {
|
||||
Info.remote = it
|
||||
if (Info.env.isActive && BuildConfig.APP_VERSION_CODE < it.magisk.versionCode)
|
||||
Info.fetchUpdate(ServiceLocator.networkService)?.let {
|
||||
if (Info.env.isActive && BuildConfig.APP_VERSION_CODE < it.versionCode)
|
||||
Notifications.updateAvailable()
|
||||
jobFinished(params, false)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.topjohnwu.magisk.core
|
||||
import android.os.Bundle
|
||||
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.core.su.TestHandler
|
||||
|
||||
class Provider : BaseProvider() {
|
||||
|
||||
@@ -13,7 +12,7 @@ class Provider : BaseProvider() {
|
||||
SuCallbackHandler.run(context!!, method, extras)
|
||||
Bundle.EMPTY
|
||||
}
|
||||
else -> TestHandler.run(method)
|
||||
else -> Bundle.EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.IntentCompat
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||
import com.topjohnwu.magisk.core.download.DownloadSession
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
|
||||
class Service : BaseService(), DownloadEngine.Session {
|
||||
class Service : BaseService(), DownloadSession {
|
||||
|
||||
private var mEngine: DownloadEngine? = null
|
||||
override val context get() = this
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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
|
||||
}
|
||||
@@ -7,9 +7,13 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
open class MagiskDB {
|
||||
|
||||
suspend fun <R> exec(
|
||||
class Literal(
|
||||
val str: String
|
||||
)
|
||||
|
||||
suspend inline fun <R> exec(
|
||||
query: String,
|
||||
mapper: suspend (Map<String, String>) -> R
|
||||
crossinline mapper: (Map<String, String>) -> R
|
||||
): List<R> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val out = Shell.cmd("magisk --sqlite '$query'").await().out
|
||||
@@ -18,13 +22,15 @@ open class MagiskDB {
|
||||
.map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.associate { it[0] to it[1] }
|
||||
.let { mapper(it) }
|
||||
.let(mapper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun exec(query: String) {
|
||||
exec(query) {}
|
||||
suspend fun exec(query: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Shell.cmd("magisk --sqlite '$query'").await()
|
||||
}
|
||||
}
|
||||
|
||||
fun Map<String, Any>.toQuery(): String {
|
||||
@@ -33,6 +39,7 @@ open class MagiskDB {
|
||||
when (it) {
|
||||
is Boolean -> if (it) "1" else "0"
|
||||
is Number -> it.toString()
|
||||
is Literal -> it.str
|
||||
else -> "\"$it\""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,24 @@ package com.topjohnwu.magisk.core.data.magiskdb
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
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() {
|
||||
|
||||
suspend fun deleteOutdated() {
|
||||
val nowSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
|
||||
val query = "DELETE FROM ${Table.POLICY} WHERE " +
|
||||
"(until > 0 AND until < $nowSeconds) OR until < 0"
|
||||
"(until > 0 AND until < strftime(\"%s\", \"now\")) OR until < 0"
|
||||
exec(query)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
suspend fun fetch(uid: Int): SuPolicy? {
|
||||
val query = "SELECT * FROM ${Table.POLICY} WHERE uid == $uid LIMIT = 1"
|
||||
val query = "$SELECT_QUERY FROM ${Table.POLICY} WHERE uid=$uid LIMIT 1"
|
||||
return exec(query, ::toPolicy).firstOrNull()
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class PolicyDao : MagiskDB() {
|
||||
}
|
||||
|
||||
suspend fun fetchAll(): List<SuPolicy> {
|
||||
val query = "SELECT * FROM ${Table.POLICY} WHERE uid/100000 == ${Const.USER_ID}"
|
||||
val query = "$SELECT_QUERY FROM ${Table.POLICY} WHERE uid/100000=${Const.USER_ID}"
|
||||
return exec(query, ::toPolicy).filterNotNull()
|
||||
}
|
||||
|
||||
@@ -43,8 +43,15 @@ class PolicyDao : MagiskDB() {
|
||||
val uid = map["uid"]?.toInt() ?: return null
|
||||
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["until"]?.toLong()?.let { policy.until = it }
|
||||
map["logging"]?.toInt()?.let { policy.logging = it != 0 }
|
||||
map["notification"]?.toInt()?.let { policy.notification = it != 0 }
|
||||
return policy
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.topjohnwu.magisk.core.data.magiskdb
|
||||
class SettingsDao : MagiskDB() {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class SettingsDao : MagiskDB() {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.topjohnwu.magisk.core.data.magiskdb
|
||||
class StringDao : MagiskDB() {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class StringDao : MagiskDB() {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.squareup.moshi.Moshi
|
||||
import com.topjohnwu.magisk.ProviderInstaller
|
||||
import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.model.DateTimeAdapter
|
||||
import com.topjohnwu.magisk.core.utils.LocaleSetting
|
||||
import okhttp3.Cache
|
||||
import okhttp3.ConnectionSpec
|
||||
@@ -72,15 +72,13 @@ fun createOkHttpClient(context: Context): OkHttpClient {
|
||||
chain.proceed(request.build())
|
||||
}
|
||||
|
||||
if (!ProviderInstaller.install(context)) {
|
||||
Info.hasGMS = false
|
||||
}
|
||||
ProviderInstaller.install(context)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun createMoshiConverterFactory(): MoshiConverterFactory {
|
||||
val moshi = Moshi.Builder().build()
|
||||
val moshi = Moshi.Builder().add(DateTimeAdapter()).build()
|
||||
return MoshiConverterFactory.create(moshi)
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ object ServiceLocator {
|
||||
val markwon by lazy { createMarkwon(AppContext) }
|
||||
val networkService by lazy {
|
||||
NetworkService(
|
||||
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
||||
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
||||
createApiService(retrofit, Const.Url.INVALID_URL),
|
||||
createApiService(retrofit, Const.Url.GITHUB_API_URL),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.app.PendingIntent
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
@@ -16,7 +15,6 @@ import androidx.collection.isNotEmpty
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.JobService
|
||||
@@ -25,18 +23,8 @@ import com.topjohnwu.magisk.core.base.IActivityExtension
|
||||
import com.topjohnwu.magisk.core.cmp
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
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.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.utils.APKInstall
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -44,13 +32,7 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.ResponseBody
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
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.
|
||||
@@ -69,16 +51,7 @@ import java.util.zip.ZipOutputStream
|
||||
* 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.
|
||||
*/
|
||||
class DownloadEngine(
|
||||
private val session: Session
|
||||
) {
|
||||
|
||||
interface Session {
|
||||
val context: Context
|
||||
|
||||
fun attachNotification(id: Int, builder: Notification.Builder)
|
||||
fun onDownloadComplete()
|
||||
}
|
||||
class DownloadEngine(session: DownloadSession) : DownloadSession by session, DownloadNotifier {
|
||||
|
||||
companion object {
|
||||
const val ACTION = "com.topjohnwu.magisk.DOWNLOAD"
|
||||
@@ -99,35 +72,37 @@ class DownloadEngine(
|
||||
}
|
||||
}
|
||||
|
||||
private fun createIntent(context: Context, subject: Subject) =
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
private fun createBroadcastIntent(context: Context, subject: Subject) =
|
||||
context.intent<com.topjohnwu.magisk.core.Receiver>()
|
||||
.setAction(ACTION)
|
||||
.putExtra(SUBJECT_KEY, subject)
|
||||
} else {
|
||||
|
||||
private fun createServiceIntent(context: Context, subject: Subject) =
|
||||
context.intent<com.topjohnwu.magisk.core.Service>()
|
||||
.setAction(ACTION)
|
||||
.putExtra(SUBJECT_KEY, subject)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||
val flag = PendingIntent.FLAG_IMMUTABLE or
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
PendingIntent.FLAG_ONE_SHOT
|
||||
val intent = createIntent(context, subject)
|
||||
return if (Build.VERSION.SDK_INT >= 34) {
|
||||
// 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.
|
||||
// As a workaround, we send the subject to a broadcast receiver and have it
|
||||
// schedule the job for us.
|
||||
val intent = createBroadcastIntent(context, subject)
|
||||
PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flag)
|
||||
} else if (Build.VERSION.SDK_INT >= 26) {
|
||||
} else {
|
||||
val intent = createServiceIntent(context, subject)
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
PendingIntent.getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||
} else {
|
||||
PendingIntent.getService(context, REQUEST_CODE, intent, flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun <T> startWithActivity(
|
||||
@@ -140,6 +115,7 @@ class DownloadEngine(
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun start(context: Context, subject: Subject) {
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
val scheduler = context.getSystemService<JobScheduler>()!!
|
||||
@@ -152,24 +128,29 @@ class DownloadEngine(
|
||||
.setTransientExtras(extras)
|
||||
.build()
|
||||
scheduler.schedule(info)
|
||||
} else if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.startForegroundService(createIntent(context, subject))
|
||||
} else {
|
||||
context.startService(createIntent(context, subject))
|
||||
val intent = createServiceIntent(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) {
|
||||
notifyUpdate(subject.notifyId)
|
||||
CoroutineScope(job + Dispatchers.IO).launch {
|
||||
try {
|
||||
val stream = network.fetchFile(subject.url).toProgressStream(subject)
|
||||
when (subject) {
|
||||
is Subject.App -> handleApp(stream, subject)
|
||||
is Subject.Module -> handleModule(stream, subject.file)
|
||||
else -> stream.copyAndClose(subject.file.outputStream())
|
||||
}
|
||||
processor.handle(stream, subject)
|
||||
val activity = AppContext.foregroundActivity
|
||||
if (activity != null && subject.autoLaunch) {
|
||||
notifyRemove(subject.notifyId)
|
||||
@@ -187,16 +168,13 @@ class DownloadEngine(
|
||||
@Synchronized
|
||||
fun reattach() {
|
||||
val builder = notifications[attachedId] ?: return
|
||||
session.attachNotification(attachedId, builder)
|
||||
attachNotification(attachedId, builder)
|
||||
}
|
||||
|
||||
private val notifications = SparseArrayCompat<Notification.Builder>()
|
||||
private var attachedId = -1
|
||||
|
||||
private val job = Job()
|
||||
|
||||
private val context get() = session.context
|
||||
private val network get() = ServiceLocator.networkService
|
||||
private fun attach(id: Int, notification: Notification.Builder) {
|
||||
attachedId = id
|
||||
attachNotification(id, notification)
|
||||
}
|
||||
|
||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
||||
@@ -223,19 +201,14 @@ class DownloadEngine(
|
||||
subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) }
|
||||
}
|
||||
|
||||
private fun attachNotification(id: Int, notification: Notification.Builder) {
|
||||
attachedId = id
|
||||
session.attachNotification(id, notification)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
override fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit) {
|
||||
val notification = (notifications[id] ?: Notifications.startProgress("").also {
|
||||
notifications[id] = it
|
||||
}).apply(editor)
|
||||
|
||||
if (attachedId < 0)
|
||||
attachNotification(id, notification)
|
||||
attach(id, notification)
|
||||
else
|
||||
Notifications.mgr.notify(id, notification.build())
|
||||
}
|
||||
@@ -255,11 +228,11 @@ class DownloadEngine(
|
||||
// There are still remaining notifications, pick one and attach to the session
|
||||
val anotherId = notifications.keyAt(0)
|
||||
val notification = notifications.valueAt(0)
|
||||
attachNotification(anotherId, notification)
|
||||
attach(anotherId, notification)
|
||||
} else {
|
||||
// No more notifications left, terminate the session
|
||||
attachedId = -1
|
||||
session.onDownloadComplete()
|
||||
onDownloadComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,90 +241,6 @@ class DownloadEngine(
|
||||
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 {
|
||||
val max = contentLength()
|
||||
val total = max.toFloat() / 1048576
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.core.ktx.withInOut
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.tasks.AppMigration
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
class DownloadProcessor(notifier: DownloadNotifier) : DownloadNotifier by notifier {
|
||||
|
||||
suspend fun handle(stream: InputStream, subject: Subject) {
|
||||
when (subject) {
|
||||
is Subject.App -> handleApp(stream, subject)
|
||||
is Subject.Module -> handleModule(stream, subject.file)
|
||||
else -> stream.copyAndClose(subject.file.outputStream())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||
val external = subject.file.outputStream()
|
||||
|
||||
if (isRunningAsStub) {
|
||||
val updateApk = StubApk.update(context)
|
||||
try {
|
||||
// Download full APK to stub update path
|
||||
stream.copyAndClose(TeeOutputStream(external, updateApk.outputStream()))
|
||||
|
||||
// Also upgrade stub
|
||||
notifyUpdate(subject.notifyId) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setContentTitle(context.getString(R.string.hide_app_title))
|
||||
.setContentText("")
|
||||
}
|
||||
|
||||
// Extract stub
|
||||
val apk = context.cachedFile("stub.apk")
|
||||
ZipFile.Builder().setFile(updateApk).get().use { zf ->
|
||||
apk.delete()
|
||||
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||
}
|
||||
|
||||
// Patch and install
|
||||
subject.intent = AppMigration.upgradeStub(context, apk)
|
||||
?: throw IOException("HideAPK patch error")
|
||||
apk.delete()
|
||||
} catch (e: Exception) {
|
||||
// If any error occurred, do not let stub load the new APK
|
||||
updateApk.delete()
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
val session = APKInstall.startSession(context)
|
||||
stream.copyAndClose(TeeOutputStream(external, session.openStream(context)))
|
||||
subject.intent = session.waitIntent()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun handleModule(src: InputStream, file: Uri) {
|
||||
val tmp = context.cachedFile("module.zip")
|
||||
try {
|
||||
// First download the entire zip into cache so we can process it
|
||||
src.writeTo(tmp)
|
||||
|
||||
val input = ZipFile.Builder().setFile(tmp).get()
|
||||
val output = ZipArchiveOutputStream(file.outputStream())
|
||||
withInOut(input, output) { zin, zout ->
|
||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/"))
|
||||
zout.closeArchiveEntry()
|
||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/"))
|
||||
zout.closeArchiveEntry()
|
||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/"))
|
||||
zout.closeArchiveEntry()
|
||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/"))
|
||||
zout.closeArchiveEntry()
|
||||
|
||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/update-binary"))
|
||||
context.assets.open("module_installer.sh").use { it.copyAll(zout) }
|
||||
zout.closeArchiveEntry()
|
||||
|
||||
zout.putArchiveEntry(ZipArchiveEntry("META-INF/com/google/android/updater-script"))
|
||||
zout.write("#MAGISK\n".toByteArray())
|
||||
zout.closeArchiveEntry()
|
||||
|
||||
// Then simply copy all entries to output
|
||||
zin.copyRawEntries(zout) { entry -> !entry.name.startsWith("META-INF") }
|
||||
}
|
||||
} finally {
|
||||
tmp.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private class TeeOutputStream(
|
||||
private val o1: OutputStream,
|
||||
private val o2: OutputStream
|
||||
) : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
o1.write(b)
|
||||
o2.write(b)
|
||||
}
|
||||
override fun write(b: ByteArray?, off: Int, len: Int) {
|
||||
o1.write(b, off, len)
|
||||
o2.write(b, off, len)
|
||||
}
|
||||
override fun close() {
|
||||
o1.close()
|
||||
o2.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
|
||||
interface DownloadSession {
|
||||
val context: Context
|
||||
fun attachNotification(id: Int, builder: Notification.Builder)
|
||||
fun onDownloadComplete()
|
||||
}
|
||||
|
||||
interface DownloadNotifier {
|
||||
val context: Context
|
||||
fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {})
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
@@ -38,7 +38,7 @@ abstract class Subject : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
class App(
|
||||
private val json: MagiskJson = Info.remote.magisk,
|
||||
private val json: UpdateInfo = Info.update,
|
||||
override val notifyId: Int = Notifications.nextId()
|
||||
) : Subject() {
|
||||
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||
|
||||
@@ -2,7 +2,11 @@ package com.topjohnwu.magisk.core.ktx
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
@@ -23,7 +27,6 @@ import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
import kotlin.String
|
||||
|
||||
fun Context.getBitmap(id: Int): Bitmap {
|
||||
var drawable = getDrawable(id)!!
|
||||
@@ -106,7 +109,7 @@ fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
|
||||
return null
|
||||
}
|
||||
// Try to find package name from PID
|
||||
val proc = RootUtils.obj?.getAppProcess(pid)
|
||||
val proc = RootUtils.getAppProcess(pid)
|
||||
if (proc == null) {
|
||||
if (uid == Process.SHELL_UID) {
|
||||
// It is possible that some apps installed are sharing UID with shell.
|
||||
|
||||
@@ -8,33 +8,25 @@ import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
inline fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||
var entry: ZipEntry? = nextEntry
|
||||
while (entry != null) {
|
||||
callback(entry)
|
||||
entry = nextEntry
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||
inStream: In,
|
||||
outStream: Out,
|
||||
inline fun <In : Closeable, Out : Closeable> withInOut(
|
||||
input: In,
|
||||
output: Out,
|
||||
withBoth: (In, Out) -> Unit
|
||||
) {
|
||||
inStream.use { reader ->
|
||||
outStream.use { writer ->
|
||||
input.use { reader ->
|
||||
output.use { writer ->
|
||||
withBoth(reader, writer)
|
||||
}
|
||||
}
|
||||
@@ -64,7 +56,7 @@ suspend inline fun InputStream.copyAndClose(
|
||||
out: OutputStream,
|
||||
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) = withStreams(this, out) { i, o -> i.copyAll(o, bufferSize, dispatcher) }
|
||||
) = withInOut(this, out) { i, o -> i.copyAll(o, bufferSize, dispatcher) }
|
||||
|
||||
@Throws(IOException::class)
|
||||
suspend inline fun InputStream.writeTo(
|
||||
@@ -92,19 +84,15 @@ inline fun <T, R> Flow<T>.concurrentMap(crossinline transform: suspend (T) -> R)
|
||||
}
|
||||
}
|
||||
|
||||
fun Long.toTime(format: DateFormat) = format.format(this).orEmpty()
|
||||
fun Long.toTime(format: DateTimeFormatter): String = format.format(Instant.ofEpochMilli(this))
|
||||
|
||||
// Some devices don't allow filenames containing ":"
|
||||
val timeFormatStandard by lazy {
|
||||
SimpleDateFormat(
|
||||
"yyyy-MM-dd'T'HH.mm.ss",
|
||||
Locale.ROOT
|
||||
)
|
||||
val timeFormatStandard: DateTimeFormatter by lazy {
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH.mm.ss").withZone(ZoneId.systemDefault())
|
||||
}
|
||||
val timeDateFormat: DateFormat by lazy {
|
||||
DateFormat.getDateTimeInstance(
|
||||
DateFormat.DEFAULT,
|
||||
DateFormat.DEFAULT,
|
||||
Locale.ROOT
|
||||
)
|
||||
val timeDateFormat: DateTimeFormatter by lazy {
|
||||
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withZone(ZoneId.systemDefault())
|
||||
}
|
||||
val dateFormat: DateTimeFormatter by lazy {
|
||||
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withZone(ZoneId.systemDefault())
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
package com.topjohnwu.magisk.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.JsonQualifier
|
||||
import com.squareup.moshi.ToJson
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.Instant
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateInfo(
|
||||
val magisk: MagiskJson = MagiskJson(),
|
||||
class UpdateJson(
|
||||
val magisk: UpdateInfo = UpdateInfo(),
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MagiskJson(
|
||||
data class UpdateInfo(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
@@ -27,11 +32,37 @@ data class ModuleJson(
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CommitInfo(
|
||||
val sha: String
|
||||
data class ReleaseAssets(
|
||||
val name: String,
|
||||
@Json(name = "browser_download_url") val url: String,
|
||||
)
|
||||
|
||||
class DateTimeAdapter {
|
||||
@ToJson
|
||||
fun toJson(date: Instant): String {
|
||||
return date.toString()
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(date: String): Instant {
|
||||
return Instant.parse(date)
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BranchInfo(
|
||||
val commit: CommitInfo
|
||||
)
|
||||
data class Release(
|
||||
@Json(name = "tag_name") val tag: String,
|
||||
val name: String,
|
||||
val prerelease: Boolean,
|
||||
val assets: List<ReleaseAssets>,
|
||||
val body: String,
|
||||
@Json(name = "created_at") val createdTime: Instant,
|
||||
) {
|
||||
val versionCode: Int get() {
|
||||
return if (tag[0] == 'v') {
|
||||
(tag.drop(1).toFloat() * 1000).toInt()
|
||||
} else {
|
||||
tag.drop(7).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,15 @@ import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.nio.ExtendedFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
data class LocalModule(
|
||||
private val path: String,
|
||||
val base: ExtendedFile,
|
||||
) : Module() {
|
||||
private val svc get() = ServiceLocator.networkService
|
||||
|
||||
@@ -24,19 +25,18 @@ data class LocalModule(
|
||||
var description: String = ""
|
||||
var updateInfo: OnlineModule? = null
|
||||
var outdated = false
|
||||
|
||||
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")
|
||||
|
||||
val updated: Boolean get() = updateFile.exists()
|
||||
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
||||
val isZygisk: Boolean get() = zygiskFolder.exists()
|
||||
val zygiskUnloaded: Boolean get() = unloaded.exists()
|
||||
private val removeFile = base.getChildFile("remove")
|
||||
private val disableFile = base.getChildFile("disable")
|
||||
private val updateFile = base.getChildFile("update")
|
||||
val zygiskFolder = base.getChildFile("zygisk")
|
||||
|
||||
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
|
||||
get() = !disableFile.exists()
|
||||
@@ -89,12 +89,11 @@ data class LocalModule(
|
||||
|
||||
init {
|
||||
runCatching {
|
||||
parseProps(Shell.cmd("dos2unix < $path/module.prop").exec().out)
|
||||
parseProps(Shell.cmd("dos2unix < $base/module.prop").exec().out)
|
||||
}
|
||||
|
||||
if (id.isEmpty()) {
|
||||
val sep = path.lastIndexOf('/')
|
||||
id = path.substring(sep + 1)
|
||||
id = base.name
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
@@ -122,14 +121,14 @@ data class LocalModule(
|
||||
|
||||
companion object {
|
||||
|
||||
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
||||
fun loaded() = RootUtils.fs.getFile(Const.MODULE_PATH).exists()
|
||||
|
||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
||||
RootUtils.fs.getFile(Const.MODULE_PATH)
|
||||
.listFiles()
|
||||
.orEmpty()
|
||||
.filter { !it.isFile && !it.isHidden }
|
||||
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
||||
.map { LocalModule(it) }
|
||||
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk.core.model.su
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
@@ -24,7 +24,7 @@ class SuLog(
|
||||
}
|
||||
|
||||
fun PackageManager.createSuLog(
|
||||
info: PackageInfo,
|
||||
info: ApplicationInfo,
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String,
|
||||
@@ -33,13 +33,12 @@ fun PackageManager.createSuLog(
|
||||
context: String,
|
||||
gids: String,
|
||||
): SuLog {
|
||||
val appInfo = info.applicationInfo
|
||||
return SuLog(
|
||||
fromUid = appInfo.uid,
|
||||
fromUid = info.uid,
|
||||
toUid = toUid,
|
||||
fromPid = fromPid,
|
||||
packageName = getNameForUid(appInfo.uid)!!,
|
||||
appName = appInfo.getLabel(this),
|
||||
packageName = getNameForUid(info.uid)!!,
|
||||
appName = info.getLabel(this),
|
||||
command = command,
|
||||
action = policy,
|
||||
target = target,
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
package com.topjohnwu.magisk.core.model.su
|
||||
|
||||
class SuPolicy(val uid: Int) {
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.MagiskDB
|
||||
|
||||
class SuPolicy(
|
||||
val uid: Int,
|
||||
var policy: Int = QUERY,
|
||||
var remain: Long = -1L,
|
||||
var logging: Boolean = true,
|
||||
var notification: Boolean = true,
|
||||
) {
|
||||
companion object {
|
||||
const val INTERACTIVE = 0
|
||||
const val QUERY = 0
|
||||
const val DENY = 1
|
||||
const val ALLOW = 2
|
||||
const val RESTRICT = 3
|
||||
}
|
||||
|
||||
var policy: Int = INTERACTIVE
|
||||
var until: Long = -1L
|
||||
var logging: Boolean = true
|
||||
var notification: Boolean = true
|
||||
|
||||
fun toMap(): MutableMap<String, Any> = mutableMapOf(
|
||||
fun toMap(): MutableMap<String, Any> {
|
||||
val until = if (remain <= 0) {
|
||||
remain
|
||||
} else {
|
||||
MagiskDB.Literal("(strftime(\"%s\", \"now\") + $remain)")
|
||||
}
|
||||
return mutableMapOf(
|
||||
"uid" to uid,
|
||||
"policy" to policy,
|
||||
"until" to until,
|
||||
@@ -20,3 +30,4 @@ class SuPolicy(val uid: Int) {
|
||||
"notification" to notification
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,114 @@
|
||||
package com.topjohnwu.magisk.core.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL
|
||||
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.STABLE_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.data.GithubPageServices
|
||||
import com.topjohnwu.magisk.core.data.RawServices
|
||||
import com.topjohnwu.magisk.core.data.GithubApiServices
|
||||
import com.topjohnwu.magisk.core.data.RawUrl
|
||||
import com.topjohnwu.magisk.core.ktx.dateFormat
|
||||
import com.topjohnwu.magisk.core.model.Release
|
||||
import com.topjohnwu.magisk.core.model.ReleaseAssets
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
class NetworkService(
|
||||
private val pages: GithubPageServices,
|
||||
private val raw: RawServices
|
||||
private val raw: RawUrl,
|
||||
private val api: GithubApiServices,
|
||||
) {
|
||||
suspend fun fetchUpdate() = safe {
|
||||
var info = when (Config.updateChannel) {
|
||||
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
||||
DEFAULT_CHANNEL -> if (BuildConfig.DEBUG) fetchDebugUpdate() else fetchStableUpdate()
|
||||
STABLE_CHANNEL -> fetchStableUpdate()
|
||||
BETA_CHANNEL -> fetchBetaUpdate()
|
||||
CANARY_CHANNEL -> fetchCanaryUpdate()
|
||||
DEBUG_CHANNEL -> fetchDebugUpdate()
|
||||
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
if (info.magisk.versionCode < Info.env.versionCode &&
|
||||
Config.updateChannel == DEFAULT_CHANNEL) {
|
||||
if (info.versionCode < Info.env.versionCode &&
|
||||
Config.updateChannel == DEFAULT_CHANNEL &&
|
||||
!BuildConfig.DEBUG
|
||||
) {
|
||||
Config.updateChannel = BETA_CHANNEL
|
||||
info = fetchBetaUpdate()
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
// UpdateInfo
|
||||
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
||||
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
||||
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
||||
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
|
||||
private suspend fun fetchCustomUpdate(url: String) = pages.fetchUpdateJSON(url)
|
||||
suspend fun fetchUpdate(version: Int) = safe {
|
||||
findRelease { it.versionCode == version }.asInfo()
|
||||
}
|
||||
|
||||
// Keep going through all release pages until we find a match
|
||||
private suspend inline fun findRelease(predicate: (Release) -> Boolean): Release? {
|
||||
var page = 1
|
||||
while (true) {
|
||||
val response = api.fetchReleases(page = page)
|
||||
val releases = response.body() ?: throw HttpException(response)
|
||||
// Remove all non Magisk releases
|
||||
releases.removeAll { it.tag[0] != 'v' && !it.tag.startsWith("canary") }
|
||||
// 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 inline fun Release?.asInfo(
|
||||
selector: (ReleaseAssets) -> Boolean = {
|
||||
// Default selector picks the non-debug APK
|
||||
it.name.run { endsWith(".apk") && !contains("debug") }
|
||||
}): UpdateInfo {
|
||||
return if (this == null) UpdateInfo()
|
||||
else if (tag[0] == 'v') asPublicInfo(selector)
|
||||
else asCanaryInfo(selector)
|
||||
}
|
||||
|
||||
private inline fun Release.asPublicInfo(selector: (ReleaseAssets) -> Boolean): UpdateInfo {
|
||||
val version = tag.drop(1)
|
||||
val date = dateFormat.format(createdTime)
|
||||
return UpdateInfo(
|
||||
version = version,
|
||||
versionCode = versionCode,
|
||||
link = assets.find(selector)!!.url,
|
||||
note = "## $date $name\n\n$body"
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun Release.asCanaryInfo(selector: (ReleaseAssets) -> Boolean): UpdateInfo {
|
||||
return UpdateInfo(
|
||||
version = name.substring(8, 16),
|
||||
versionCode = versionCode,
|
||||
link = assets.find(selector)!!.url,
|
||||
note = "## $name\n\n$body"
|
||||
)
|
||||
}
|
||||
|
||||
// Version number: debug == beta >= stable
|
||||
|
||||
// Find the latest non-prerelease
|
||||
private suspend fun fetchStableUpdate() = api.fetchLatestRelease().asInfo()
|
||||
|
||||
// Find the latest release, regardless whether it's prerelease
|
||||
private suspend fun fetchBetaUpdate() = findRelease { true }.asInfo()
|
||||
|
||||
private suspend fun fetchDebugUpdate() =
|
||||
findRelease { true }.asInfo { it.name == "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? {
|
||||
return try {
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ByteArrayStream extends ByteArrayOutputStream {
|
||||
|
||||
@@ -27,4 +28,8 @@ public class ByteArrayStream extends ByteArrayOutputStream {
|
||||
public ByteArrayInputStream getInputStream() {
|
||||
return new ByteArrayInputStream(buf, 0, count);
|
||||
}
|
||||
|
||||
public ByteBuffer toByteBuffer() {
|
||||
return ByteBuffer.wrap(buf, 0, count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,7 +510,7 @@ public class SignApk {
|
||||
privateKey[0] = key;
|
||||
|
||||
// Generate, in memory, an APK signed using standard JAR Signature Scheme.
|
||||
ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream();
|
||||
ByteArrayStream v1SignedApkBuf = new ByteArrayStream();
|
||||
JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf);
|
||||
// Use maximum compression for compressed entries because the APK lives forever on
|
||||
// the system partition.
|
||||
@@ -519,8 +519,7 @@ public class SignApk {
|
||||
copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
|
||||
signFile(manifest, publicKey, privateKey, timestamp, outputJar);
|
||||
outputJar.close();
|
||||
ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray());
|
||||
v1SignedApkBuf.reset();
|
||||
ByteBuffer v1SignedApk = v1SignedApkBuf.toByteBuffer();
|
||||
|
||||
ByteBuffer[] outputChunks;
|
||||
List<ApkSignerV2.SignerConfig> signerConfigs = createV2SignerConfigs(privateKey, publicKey,
|
||||
|
||||
@@ -64,13 +64,13 @@ object SuCallbackHandler {
|
||||
val pm = context.packageManager
|
||||
|
||||
val log = runCatching {
|
||||
pm.getPackageInfo(fromUid, pid)?.let {
|
||||
pm.getPackageInfo(fromUid, pid)?.applicationInfo?.let {
|
||||
pm.createSuLog(it, toUid, pid, command, policy, target, seContext, gids)
|
||||
}
|
||||
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy, target, seContext, gids)
|
||||
|
||||
if (notify)
|
||||
notify(context, log.action == SuPolicy.ALLOW, log.appName)
|
||||
notify(context, log.action >= SuPolicy.ALLOW, log.appName)
|
||||
|
||||
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||
}
|
||||
@@ -86,7 +86,7 @@ object SuCallbackHandler {
|
||||
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
|
||||
}.getOrNull() ?: "[UID] $uid"
|
||||
|
||||
notify(context, policy == SuPolicy.ALLOW, appName)
|
||||
notify(context, policy >= SuPolicy.ALLOW, appName)
|
||||
}
|
||||
|
||||
private fun notify(context: Context, granted: Boolean, appName: String) {
|
||||
|
||||
@@ -62,7 +62,7 @@ class SuRequestHandler(
|
||||
return false
|
||||
}
|
||||
output = File(fifo)
|
||||
policy = SuPolicy(uid)
|
||||
policy = policyDB.fetch(uid) ?: SuPolicy(uid)
|
||||
try {
|
||||
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
||||
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||
@@ -81,15 +81,17 @@ class SuRequestHandler(
|
||||
return true
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
suspend fun respond(action: Int, time: Long) {
|
||||
if (action == SuPolicy.ALLOW && Config.suRestrict) {
|
||||
policy.policy = SuPolicy.RESTRICT
|
||||
} else {
|
||||
policy.policy = action
|
||||
policy.until = until
|
||||
}
|
||||
if (time >= 0) {
|
||||
policy.remain = TimeUnit.MINUTES.toSeconds(time)
|
||||
} else {
|
||||
policy.remain = time
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
@@ -100,7 +102,7 @@ class SuRequestHandler(
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
if (until >= 0) {
|
||||
if (time >= 0) {
|
||||
policyDB.update(policy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.os.Bundle
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Runnable
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
|
||||
object TestHandler {
|
||||
|
||||
object LogList : CallbackList<String>(Runnable::run) {
|
||||
override fun onAddElement(e: String) {
|
||||
Timber.i(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun run(method: String): Bundle {
|
||||
var reason: String? = null
|
||||
|
||||
fun prerequisite(): Boolean {
|
||||
// Make sure the Magisk app can get root
|
||||
val shell = Shell.getShell()
|
||||
if (!shell.isRoot) {
|
||||
reason = "shell not root"
|
||||
return false
|
||||
}
|
||||
|
||||
// Make sure the root service is running
|
||||
RootUtils.Connection.await()
|
||||
return true
|
||||
}
|
||||
|
||||
fun setup(): Boolean {
|
||||
return runBlocking {
|
||||
MagiskInstaller.Emulator(LogList, LogList).exec()
|
||||
}
|
||||
}
|
||||
|
||||
fun test(): Boolean {
|
||||
// Make sure Zygisk works correctly
|
||||
if (!Info.isZygiskEnabled) {
|
||||
reason = "zygisk not enabled"
|
||||
return false
|
||||
}
|
||||
|
||||
// Clear existing grant for ADB shell
|
||||
runBlocking {
|
||||
ServiceLocator.policyDB.delete(2000)
|
||||
Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW
|
||||
Config.prefs.edit().commit()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val result = prerequisite() && runCatching {
|
||||
when (method) {
|
||||
"setup" -> setup()
|
||||
"test" -> test()
|
||||
else -> {
|
||||
reason = "unknown method"
|
||||
false
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
reason = it.stackTraceToString()
|
||||
false
|
||||
}
|
||||
|
||||
return Bundle().apply {
|
||||
putBoolean("result", result)
|
||||
if (reason != null) putString("reason", reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.app.Activity
|
||||
import android.app.ActivityOptions
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
@@ -13,7 +14,6 @@ import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.R
|
||||
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.writeTo
|
||||
import com.topjohnwu.magisk.core.signing.JarMap
|
||||
@@ -23,11 +23,9 @@ import com.topjohnwu.magisk.core.utils.Keygen
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Runnable
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.SecureRandom
|
||||
@@ -38,6 +36,7 @@ object AppMigration {
|
||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
||||
private const val ALPHADOTS = "$ALPHA....."
|
||||
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
private const val TEST_PKG_NAME = "$APP_PACKAGE_NAME.test"
|
||||
|
||||
// Some arbitrary limit
|
||||
const val MAX_LABEL_LENGTH = 32
|
||||
@@ -125,28 +124,23 @@ object AppMigration {
|
||||
apk: File, out: OutputStream,
|
||||
pkg: String, label: CharSequence
|
||||
): Boolean {
|
||||
val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
|
||||
val origLabel = info.applicationInfo.nonLocalizedLabel.toString()
|
||||
val pm = context.packageManager
|
||||
val info = pm.getPackageArchiveInfo(apk.path, 0)?.applicationInfo ?: return false
|
||||
val origLabel = info.nonLocalizedLabel.toString()
|
||||
try {
|
||||
JarMap.open(apk, true).use { jar ->
|
||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||
val xml = AXML(jar.getRawData(je))
|
||||
val generator = classNameGenerator()
|
||||
|
||||
if (!xml.patchStrings {
|
||||
for (i in it.indices) {
|
||||
val s = it[i]
|
||||
if (s.contains(APP_PACKAGE_NAME)) {
|
||||
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()
|
||||
val p = xml.patchStrings {
|
||||
when {
|
||||
it.contains(APP_PACKAGE_NAME) -> it.replace(APP_PACKAGE_NAME, pkg)
|
||||
it.contains(PLACEHOLDER) -> generator.next()
|
||||
it == origLabel -> label.toString()
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
}) {
|
||||
return false
|
||||
}
|
||||
if (!p) return false
|
||||
|
||||
// Write apk changes
|
||||
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||
@@ -160,51 +154,87 @@ object AppMigration {
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchApp(activity: Activity, pkg: String) {
|
||||
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||
private fun patchTest(apk: File, out: File, pkg: String): Boolean {
|
||||
try {
|
||||
JarMap.open(apk, true).use { jar ->
|
||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||
val xml = AXML(jar.getRawData(je))
|
||||
val p = xml.patchStrings {
|
||||
when (it) {
|
||||
APP_PACKAGE_NAME -> pkg
|
||||
TEST_PKG_NAME -> "$pkg.test"
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
if (!p) return false
|
||||
|
||||
// Write apk changes
|
||||
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||
val keys = Keygen()
|
||||
out.outputStream().use { SignApk.sign(keys.cert, keys.key, jar, it) }
|
||||
return true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchApp(context: Context, pkg: String) {
|
||||
val intent = context.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||
intent.putExtra(Const.Key.PREV_CONFIG, Config.toBundle())
|
||||
val options = ActivityOptions.makeBasic()
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
options.setShareIdentityEnabled(true)
|
||||
}
|
||||
activity.startActivity(intent, options.toBundle())
|
||||
activity.finish()
|
||||
context.startActivity(intent, options.toBundle())
|
||||
if (context is Activity) {
|
||||
context.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
||||
val stub = File(activity.cacheDir, "stub.apk")
|
||||
suspend fun patchAndHide(context: Context, label: String, pkg: String? = null): Boolean {
|
||||
val stub = File(context.cacheDir, "stub.apk")
|
||||
try {
|
||||
activity.assets.open("stub.apk").writeTo(stub)
|
||||
context.assets.open("stub.apk").writeTo(stub)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// Generate a new random package name and signature
|
||||
val repack = File(activity.cacheDir, "patched.apk")
|
||||
val pkg = genPackageName()
|
||||
// Generate a new random signature and package name if needed
|
||||
val pkg = pkg ?: genPackageName()
|
||||
Config.keyStoreRaw = ""
|
||||
|
||||
if (!patch(activity, stub, FileOutputStream(repack), pkg, label))
|
||||
// Check and patch the test APK
|
||||
try {
|
||||
val info = context.packageManager.getApplicationInfo(TEST_PKG_NAME, 0)
|
||||
val testApk = File(info.sourceDir)
|
||||
val testRepack = File(context.cacheDir, "test.apk")
|
||||
if (!patchTest(testApk, testRepack, pkg))
|
||||
return false
|
||||
val cmd = "adb_pm_install $testRepack $pkg.test"
|
||||
if (!Shell.cmd(cmd).exec().isSuccess)
|
||||
return false
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
}
|
||||
|
||||
val repack = File(context.cacheDir, "patched.apk")
|
||||
repack.outputStream().use {
|
||||
if (!patch(context, stub, it, pkg, label))
|
||||
return false
|
||||
}
|
||||
|
||||
// Install and auto launch app
|
||||
val session = APKInstall.startSession(activity, pkg, onFailure) {
|
||||
launchApp(activity, pkg)
|
||||
}
|
||||
|
||||
val cmd = "adb_pm_install $repack $pkg"
|
||||
if (Shell.cmd(cmd).exec().isSuccess) {
|
||||
Config.suManager = pkg
|
||||
val cmd = "touch $AppApkPath; 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)
|
||||
Shell.cmd("touch $AppApkPath").exec()
|
||||
launchApp(context, pkg)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
session.waitIntent()?.let { activity.startActivity(it) } ?: return false
|
||||
return true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -215,14 +245,25 @@ object AppMigration {
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
val onFailure = Runnable {
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
patchAndHide(activity, label)
|
||||
}
|
||||
if (!success) {
|
||||
dialog.dismiss()
|
||||
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
patchAndHide(activity, label, onFailure)
|
||||
}
|
||||
if (!success) onFailure.run()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -233,30 +274,11 @@ object AppMigration {
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
val onFailure = Runnable {
|
||||
dialog.dismiss()
|
||||
if (!restoreApp(activity)) {
|
||||
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
val apk = StubApk.current(activity)
|
||||
val session = APKInstall.startSession(activity, APP_PACKAGE_NAME, onFailure) {
|
||||
launchApp(activity, APP_PACKAGE_NAME)
|
||||
dialog.dismiss()
|
||||
}
|
||||
Config.suManager = ""
|
||||
val cmd = "touch $AppApkPath; 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? {
|
||||
val label = context.applicationInfo.nonLocalizedLabel
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.unzip
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -47,20 +46,14 @@ open class FlashZip(
|
||||
}
|
||||
}
|
||||
|
||||
val isValid = try {
|
||||
zipFile.unzip(installDir, "META-INF/com/google/android", true)
|
||||
val script = File(installDir, "updater-script")
|
||||
script.readText().contains("#MAGISK")
|
||||
try {
|
||||
val binary = File(installDir, "update-binary")
|
||||
AppContext.assets.open("module_installer.sh").use { it.writeTo(binary) }
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unzip error")
|
||||
throw e
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
console.add("! This zip is not a Magisk module!")
|
||||
return false
|
||||
}
|
||||
|
||||
console.add("- Installing ${mUri.displayName}")
|
||||
|
||||
return Shell.cmd("sh $installDir/update-binary dummy 1 \'$zipFile\'")
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.system.OsConstants.O_WRONLY
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
@@ -15,13 +14,9 @@ import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.core.ktx.reboot
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.utils.DummyList
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
@@ -51,7 +46,6 @@ import java.io.OutputStream
|
||||
import java.io.PushbackInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.SecureRandom
|
||||
import java.util.Arrays
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@@ -87,10 +81,12 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
}
|
||||
|
||||
private fun findImage(slot: String): Boolean {
|
||||
val bootPath = (
|
||||
"(RECOVERYMODE=${Config.recovery} " +
|
||||
"SLOT=$slot find_boot_image; " +
|
||||
"echo \$BOOTIMAGE)").fsh()
|
||||
val cmd =
|
||||
"RECOVERYMODE=${Config.recovery} " +
|
||||
"VENDORBOOT=${Info.isVendorBoot} " +
|
||||
"SLOT=$slot " +
|
||||
"find_boot_image; echo \$BOOTIMAGE"
|
||||
val bootPath = ("($cmd)").fsh()
|
||||
if (bootPath.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
@@ -138,7 +134,6 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
if (entry != null) {
|
||||
val magisk32 = File(installDir, "magisk32")
|
||||
zf.getInputStream(entry).writeTo(magisk32)
|
||||
magisk32.setExecutable(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,11 +148,15 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
Os.symlink(lib.path, "$installDir/$name")
|
||||
}
|
||||
|
||||
// Also symlink magisk32 on 64-bit devices that supports 32-bit
|
||||
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir")
|
||||
.get(info) as String?
|
||||
if (lib32 != null) {
|
||||
Os.symlink("$lib32/libmagisk.so", "$installDir/magisk32");
|
||||
// Also extract magisk32 on 64-bit devices that supports 32-bit
|
||||
val abi32 = Const.CPU_ABI_32
|
||||
if (Process.is64Bit() && abi32 != null) {
|
||||
val name = "lib/$abi32/libmagisk.so"
|
||||
val entry = javaClass.classLoader!!.getResourceAsStream(name)
|
||||
if (entry != null) {
|
||||
val magisk32 = File(installDir, "magisk32")
|
||||
entry.writeTo(magisk32)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +194,8 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyAll(it) }
|
||||
private suspend fun InputStream.copyAndCloseOut(out: OutputStream) =
|
||||
out.use { copyAll(it, 1024 * 1024) }
|
||||
|
||||
private class NoAvailableStream(s: InputStream) : FilterInputStream(s) {
|
||||
// Make sure available is never called on the actual stream and always return 0
|
||||
@@ -229,8 +229,8 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
console.add("- Processing tar file")
|
||||
var entry: TarArchiveEntry? = tarIn.nextEntry
|
||||
|
||||
fun TarArchiveEntry.decompressedStream(): InputStream {
|
||||
val stream = if (name.endsWith(".lz4"))
|
||||
fun decompressedStream(): InputStream {
|
||||
val stream = if (tarIn.currentEntry.name.endsWith(".lz4"))
|
||||
FramedLZ4CompressorInputStream(tarIn, true) else tarIn
|
||||
return NoAvailableStream(stream)
|
||||
}
|
||||
@@ -256,9 +256,9 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
|
||||
if (bootItem != null) {
|
||||
console.add("-- Extracting: ${bootItem.name}")
|
||||
entry.decompressedStream().copyAndCloseOut(bootItem.file.newOutputStream())
|
||||
decompressedStream().copyAndCloseOut(bootItem.file.newOutputStream())
|
||||
} else if (entry.name.contains("vbmeta.img")) {
|
||||
val rawData = entry.decompressedStream().readBytes()
|
||||
val rawData = decompressedStream().readBytes()
|
||||
// Valid vbmeta.img should be at least 256 bytes
|
||||
if (rawData.size < 256)
|
||||
continue
|
||||
@@ -291,7 +291,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
} else {
|
||||
console.add("-- Copying : ${entry.name}")
|
||||
tarOut.putArchiveEntry(entry)
|
||||
tarIn.copyAll(tarOut, bufferSize = 1024 * 1024)
|
||||
tarIn.copyAll(tarOut)
|
||||
tarOut.closeArchiveEntry()
|
||||
}
|
||||
entry = tarIn.nextEntry ?: break
|
||||
@@ -433,7 +433,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
|
||||
// Process input file
|
||||
try {
|
||||
PushbackInputStream(uri.inputStream(), 512).use { src ->
|
||||
PushbackInputStream(uri.inputStream().buffered(1024 * 1024), 512).use { src ->
|
||||
val head = ByteArray(512)
|
||||
if (src.read(head) != head.size) {
|
||||
console.add("! Invalid input file")
|
||||
@@ -442,12 +442,13 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
src.unread(head)
|
||||
|
||||
val magic = head.copyOf(4)
|
||||
val tarMagic = Arrays.copyOfRange(head, 257, 262)
|
||||
val tarMagic = head.copyOfRange(257, 262)
|
||||
|
||||
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
|
||||
// tar file
|
||||
outFile = MediaStoreUtils.getFile("$destName.tar")
|
||||
outStream = TarArchiveOutputStream(outFile.uri.outputStream()).also {
|
||||
val os = outFile.uri.outputStream().buffered(1024 * 1024)
|
||||
outStream = TarArchiveOutputStream(os).also {
|
||||
it.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR)
|
||||
it.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU)
|
||||
}
|
||||
@@ -504,7 +505,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
bootItem.file = newBoot
|
||||
bootItem.copyTo(outStream as TarArchiveOutputStream)
|
||||
} else {
|
||||
newBoot.newInputStream().copyAndClose(outStream)
|
||||
newBoot.newInputStream().use { it.copyAll(outStream, 1024 * 1024) }
|
||||
}
|
||||
newBoot.delete()
|
||||
|
||||
@@ -518,6 +519,8 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
outFile.delete()
|
||||
Timber.e(e)
|
||||
return false
|
||||
} finally {
|
||||
outStream.close()
|
||||
}
|
||||
|
||||
// Fix up binaries
|
||||
@@ -585,6 +588,8 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
|
||||
protected suspend fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
||||
|
||||
protected fun restore() = findImage() && "restore_imgs $srcBoot".sh().isSuccess
|
||||
|
||||
protected fun uninstall() = "run_uninstaller $AppApkPath".sh().isSuccess
|
||||
|
||||
@WorkerThread
|
||||
@@ -599,6 +604,8 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
if (result)
|
||||
return true
|
||||
|
||||
// Not every operation initializes installDir
|
||||
if (::installDir.isInitialized)
|
||||
Shell.cmd("rm -rf $installDir").submit()
|
||||
return false
|
||||
}
|
||||
@@ -608,11 +615,10 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
}
|
||||
}
|
||||
|
||||
abstract class MagiskInstaller(
|
||||
abstract class ConsoleInstaller(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstallImpl(console, logs) {
|
||||
|
||||
override suspend fun exec(): Boolean {
|
||||
val success = super.exec()
|
||||
if (success) {
|
||||
@@ -622,40 +628,51 @@ abstract class MagiskInstaller(
|
||||
}
|
||||
return success
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CallBackInstaller : MagiskInstallImpl(DummyList, DummyList) {
|
||||
suspend fun exec(callback: (Boolean) -> Unit): Boolean {
|
||||
val success = exec()
|
||||
callback(success)
|
||||
return success
|
||||
}
|
||||
}
|
||||
|
||||
class MagiskInstaller {
|
||||
|
||||
class Patch(
|
||||
private val uri: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(console, logs) {
|
||||
) : ConsoleInstaller(console, logs) {
|
||||
override suspend fun operations() = patchFile(uri)
|
||||
}
|
||||
|
||||
class SecondSlot(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(console, logs) {
|
||||
) : ConsoleInstaller(console, logs) {
|
||||
override suspend fun operations() = secondSlot()
|
||||
}
|
||||
|
||||
class Direct(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(console, logs) {
|
||||
) : ConsoleInstaller(console, logs) {
|
||||
override suspend fun operations() = direct()
|
||||
}
|
||||
|
||||
class Emulator(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(console, logs) {
|
||||
) : ConsoleInstaller(console, logs) {
|
||||
override suspend fun operations() = fixEnv()
|
||||
}
|
||||
|
||||
class Uninstall(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstallImpl(console, logs) {
|
||||
) : ConsoleInstaller(console, logs) {
|
||||
override suspend fun operations() = uninstall()
|
||||
|
||||
override suspend fun exec(): Boolean {
|
||||
@@ -669,19 +686,11 @@ abstract class MagiskInstaller(
|
||||
}
|
||||
}
|
||||
|
||||
class FixEnv(private val callback: () -> Unit) : MagiskInstallImpl(DummyList, DummyList) {
|
||||
override suspend fun operations() = fixEnv()
|
||||
class Restore : CallBackInstaller() {
|
||||
override suspend fun operations() = restore()
|
||||
}
|
||||
|
||||
override suspend fun exec(): Boolean {
|
||||
val success = super.exec()
|
||||
callback()
|
||||
context.toast(
|
||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
if (success)
|
||||
UiThreadHandler.handler.postDelayed(5000) { reboot() }
|
||||
return success
|
||||
}
|
||||
class FixEnv : CallBackInstaller() {
|
||||
override suspend fun operations() = fixEnv()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class AXML(b: ByteArray) {
|
||||
* Followed by an array of uint32_t with size = number of strings
|
||||
* Each entry points to an offset into the string data
|
||||
*/
|
||||
fun patchStrings(patchFn: (Array<String>) -> Unit): Boolean {
|
||||
fun patchStrings(mapFn: (String) -> String): Boolean {
|
||||
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
||||
|
||||
fun findStringPool(): Int {
|
||||
@@ -65,7 +65,9 @@ class AXML(b: ByteArray) {
|
||||
}
|
||||
|
||||
val strArr = strList.toTypedArray()
|
||||
patchFn(strArr)
|
||||
for (i in strArr.indices) {
|
||||
strArr[i] = mapFn(strArr[i])
|
||||
}
|
||||
|
||||
// Write everything before string data, will patch values later
|
||||
val baos = RawByteStream()
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.topjohnwu.magisk.core.utils;
|
||||
|
||||
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.util.zip.ZipEntry;
|
||||
|
||||
public class Desugar {
|
||||
public static FileTime getLastModifiedTime(ZipEntry entry) {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
return entry.getLastModifiedTime();
|
||||
} else {
|
||||
return FileTime.fromMillis(entry.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
public static FileTime getLastAccessTime(ZipEntry entry) {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
return entry.getLastAccessTime();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static FileTime getCreationTime(ZipEntry entry) {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
return entry.getCreationTime();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Within {@link ZipArchiveOutputStream#copyFromZipInputStream}, we redirect the method call
|
||||
* {@link ZipUtil#checkRequestedFeatures} to this method. This is safe because the only usage
|
||||
* of copyFromZipInputStream is in {@link ZipArchiveOutputStream#addRawArchiveEntry},
|
||||
* which does not need to actually understand the content of the zip entry. By removing
|
||||
* this feature check, we can modify zip files using unsupported compression methods.
|
||||
*/
|
||||
public static void checkRequestedFeatures(final ZipArchiveEntry ze) {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,10 @@ import android.net.NetworkRequest
|
||||
import android.os.PowerManager
|
||||
import androidx.collection.ArraySet
|
||||
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.ktx.registerRuntimeReceiver
|
||||
|
||||
class NetworkObserver(context: Context): DefaultLifecycleObserver {
|
||||
class NetworkObserver(context: Context) {
|
||||
private val manager = context.getSystemService<ConnectivityManager>()!!
|
||||
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
@@ -55,20 +52,17 @@ class NetworkObserver(context: Context): DefaultLifecycleObserver {
|
||||
manager.registerNetworkCallback(request, networkCallback)
|
||||
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
||||
context.applicationContext.registerRuntimeReceiver(receiver, filter)
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
postCurrentState()
|
||||
}
|
||||
|
||||
private fun postCurrentState() {
|
||||
postValue(manager.getNetworkCapabilities(manager.activeNetwork)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false)
|
||||
fun postCurrentState() {
|
||||
postValue(
|
||||
manager.getNetworkCapabilities(manager.activeNetwork)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true
|
||||
)
|
||||
}
|
||||
|
||||
private fun postValue(b: Boolean) {
|
||||
Info.remote = Info.EMPTY_REMOTE
|
||||
Info.resetUpdate()
|
||||
Info.isConnected.postValue(b)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.utils;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LifecycleDispatcher;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
// Use Java to bypass Kotlin internal visibility modifier
|
||||
public class ProcessLifecycle {
|
||||
public static void init(@NonNull Context context) {
|
||||
LifecycleDispatcher.init(context);
|
||||
ProcessLifecycleOwner.init$lifecycle_process_release(context);
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,14 @@ import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.system.Os
|
||||
import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.locks.AbstractQueuedSynchronizer
|
||||
@@ -43,16 +46,7 @@ class RootUtils(stub: Any?) : RootService() {
|
||||
return object : IRootUtils.Stub() {
|
||||
override fun getAppProcess(pid: Int) = safe(null) { getAppProcessImpl(pid) }
|
||||
override fun getFileSystem(): IBinder = FileSystemManager.getService()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> safe(default: T, block: () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
// The process died unexpectedly
|
||||
Timber.e(e)
|
||||
default
|
||||
override fun addSystemlessHosts() = safe(false) { addSystemlessHostsImpl() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +72,26 @@ class RootUtils(stub: Any?) : RootService() {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun addSystemlessHostsImpl(): Boolean {
|
||||
val module = File(Const.MODULE_PATH, "hosts")
|
||||
if (module.exists()) return true
|
||||
val hosts = File(module, "system/etc/hosts")
|
||||
if (!hosts.parentFile.mkdirs()) return false
|
||||
File(module, "module.prop").outputStream().writer().use {
|
||||
it.write("""
|
||||
id=hosts
|
||||
name=Systemless Hosts
|
||||
version=1.0
|
||||
versionCode=1
|
||||
author=Magisk
|
||||
description=Magisk app built-in systemless hosts module
|
||||
""".trimIndent())
|
||||
}
|
||||
File("/system/etc/hosts").copyTo(hosts)
|
||||
File(module, "update").createNewFile()
|
||||
return true
|
||||
}
|
||||
|
||||
object Connection : AbstractQueuedSynchronizer(), ServiceConnection {
|
||||
init {
|
||||
state = 1
|
||||
@@ -131,11 +145,25 @@ class RootUtils(stub: Any?) : RootService() {
|
||||
return field
|
||||
}
|
||||
private set
|
||||
var obj: IRootUtils? = null
|
||||
private var obj: IRootUtils? = null
|
||||
get() {
|
||||
Connection.await()
|
||||
return field
|
||||
}
|
||||
private set
|
||||
|
||||
fun getAppProcess(pid: Int) = safe(null) { obj?.getAppProcess(pid) }
|
||||
|
||||
suspend fun addSystemlessHosts() =
|
||||
withContext(Dispatchers.IO) { safe(false) { obj?.addSystemlessHosts() ?: false } }
|
||||
|
||||
private inline fun <T> safe(default: T, block: () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
// The process died unexpectedly
|
||||
Timber.e(e)
|
||||
default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
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.magisk.test.Environment.Companion.UPGRADE_TEST
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
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 = 4
|
||||
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/fonts/newfile should exist",
|
||||
RootUtils.fs.getFile("/system/fonts/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()
|
||||
assertArrayEquals(
|
||||
"/system/app/EasterEgg should be replaced",
|
||||
egg,
|
||||
arrayOf("newfile")
|
||||
)
|
||||
}
|
||||
|
||||
@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 })
|
||||
assertTrue(
|
||||
"Uninstaller of $REMOVE_TEST should be run",
|
||||
RootUtils.fs.getFile(Environment.REMOVE_TEST_MARKER).exists()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testModuleUpgrade() {
|
||||
val module = modules.find { it.id == UPGRADE_TEST }
|
||||
assertNotNull("$UPGRADE_TEST is not installed", module)
|
||||
module!!
|
||||
assertFalse("$UPGRADE_TEST should be disabled", module.enable)
|
||||
assertTrue(
|
||||
"$UPGRADE_TEST should be updated",
|
||||
module.base.getChildFile("post-fs-data.sh").exists()
|
||||
)
|
||||
assertFalse(
|
||||
"$UPGRADE_TEST should be updated",
|
||||
module.base.getChildFile("service.sh").exists()
|
||||
)
|
||||
}
|
||||
}
|
||||
27
app/core/src/main/java/com/topjohnwu/magisk/test/BaseTest.kt
Normal file
27
app/core/src/main/java/com/topjohnwu/magisk/test/BaseTest.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
288
app/core/src/main/java/com/topjohnwu/magisk/test/Environment.kt
Normal file
288
app/core/src/main/java/com/topjohnwu/magisk/test/Environment.kt
Normal file
@@ -0,0 +1,288 @@
|
||||
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_UPDATE_PATH = "/data/adb/modules_update"
|
||||
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 REMOVE_TEST_MARKER = "/dev/.remove_test_removed"
|
||||
const val EMPTY_ZYGISK = "empty_zygisk"
|
||||
const val UPGRADE_TEST = "upgrade_test"
|
||||
}
|
||||
|
||||
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/fonts/newfile
|
||||
val etc = path.getChildFile("system").getChildFile("fonts")
|
||||
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())
|
||||
|
||||
// Create /system/app/EasterEgg/newfile
|
||||
assertTrue(error, egg.getChildFile("newfile").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 setupSystemlessHost() {
|
||||
val error = "hosts setup failed"
|
||||
assertTrue(error, runBlocking { RootUtils.addSystemlessHosts() })
|
||||
assertTrue(error, RootUtils.fs.getFile(Const.MODULE_PATH).getChildFile("hosts").exists())
|
||||
}
|
||||
|
||||
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())
|
||||
// Create uninstaller script
|
||||
path.getChildFile("uninstall.sh").newOutputStream().writer().use {
|
||||
it.write("touch $REMOVE_TEST_MARKER")
|
||||
}
|
||||
assertTrue(error, path.getChildFile("service.sh").createNewFile())
|
||||
module.remove = true
|
||||
|
||||
assertTrue(error, Shell.cmd("set_default_perm $path").exec().isSuccess)
|
||||
}
|
||||
|
||||
private fun setupUpgradeModule(root: ExtendedFile, update: ExtendedFile) {
|
||||
val error = "$UPGRADE_TEST setup failed"
|
||||
val oldPath = root.getChildFile(UPGRADE_TEST)
|
||||
val newPath = update.getChildFile(UPGRADE_TEST)
|
||||
|
||||
// Create an existing module but mark as "disable
|
||||
val module = LocalModule(oldPath)
|
||||
assertTrue(error, oldPath.mkdirs())
|
||||
module.enable = false
|
||||
// Install service.sh into the old module
|
||||
assertTrue(error, oldPath.getChildFile("service.sh").createNewFile())
|
||||
|
||||
// Create an upgrade module
|
||||
assertTrue(error, newPath.mkdirs())
|
||||
// Install post-fs-data.sh into the new module
|
||||
assertTrue(error, newPath.getChildFile("post-fs-data.sh").createNewFile())
|
||||
|
||||
assertTrue(error, Shell.cmd(
|
||||
"set_default_perm $oldPath",
|
||||
"set_default_perm $newPath",
|
||||
).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)
|
||||
val update = RootUtils.fs.getFile(MODULE_UPDATE_PATH)
|
||||
if (mount()) { setupMountTest(update) }
|
||||
if (preinit()) { setupSepolicyRuleModule(update) }
|
||||
setupSystemlessHost()
|
||||
setupEmptyZygiskModule(update)
|
||||
setupInvalidZygiskModule(update)
|
||||
setupRemoveModule(root)
|
||||
setupUpgradeModule(root, update)
|
||||
}
|
||||
|
||||
@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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,154 +2,130 @@
|
||||
|
||||
<!--Sections-->
|
||||
<string name="modules">الإضافات</string>
|
||||
<string name="superuser">المستخدم الخارق</string>
|
||||
<string name="superuser">صلاحية الروت</string>
|
||||
<string name="logs">السجلات</string>
|
||||
<string name="settings">الإعدادات</string>
|
||||
<string name="install">التثبيت</string>
|
||||
<string name="section_home">الرئيسية</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="no_connection">لا يوجد إتصال</string>
|
||||
<string name="app_changelog">تفاصيل التحديث</string>
|
||||
<string name="loading">جارٍ التحميل...</string>
|
||||
<string name="update">تحديث</string>
|
||||
<string name="not_available">غير متاح</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 فقط من صفحة GitHub الرسمية. الملفات من مصادر غير معروفة قد تكون ضارة!</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سيتم إزالة الروت!\nأي تخزين داخلي تم فك تشفيره باستخدام Magisk سيتم إعادة تشفيره!</string>
|
||||
<string name="home_support_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="keep_force_encryption">فرض التشفير الإجباري</string>
|
||||
<string name="keep_dm_verity">فرض تشفيرات AVB2.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 tarfile (*.tar) أو payload.bin (*.bin)</string>
|
||||
<string name="reboot_delay_toast">سيتم إعادة التشغيل في 5 ثوانٍ...</string>
|
||||
<string name="direct_install">تثبيت مباشر (موصى بها)</string>
|
||||
<string name="install_inactive_slot">التثبيت على المنطقة الغير نشطة (بعد OTA)</string>
|
||||
<string name="install_inactive_slot_msg">سيُجبر جهازك للاقلاع على المنطقة الغير النشطة بعد إعادة التشغيل!\n استخدم هذا الخيار فقط بعد الانتهاء من OTA. استمرار؟</string>
|
||||
<string name="setup_title">إعدادات إضافية</string>
|
||||
<string name="select_patch_file">حدد و عدل ملفاً</string>
|
||||
<string name="patch_file_msg">اختر ملف (*.img) أو ملف odin (*.tar)</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">نظرًا لأن تطبيقًا ما يحجب طلب المستخدم الخارق، لا يمكن لـ Magisk التحقق من استجابتك</string>
|
||||
<string name="su_request_title">طلبات صلاحية الروت</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="prompt">طلب</string>
|
||||
<string name="grant">سماح</string>
|
||||
<string name="su_warning">يمنح حق الوصول الكامل إلى جهازك. ارفض إذا كنت غير متأكد!</string>
|
||||
<string name="forever">للأبد</string>
|
||||
<string name="once">مرة واحدة</string>
|
||||
<string name="tenmin">جار0 دقائق</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">%جار$s تم منحه حقوق المستخدم الخارق</string>
|
||||
<string name="su_deny_toast">%جار$s تم رفض حقوق المستخدم الخارق</string>
|
||||
<string name="su_snack_grant">تم منح حقوق المستخدم الخارق لـ %جار$s</string>
|
||||
<string name="su_snack_deny">تم رفض حقوق المستخدم الخارق لـ %جار$s</string>
|
||||
<string name="su_snack_notif_on">تم تفعيل الإشعارات لـ %جار$s</string>
|
||||
<string name="su_snack_notif_off">تم تعطيل الإشعارات لـ %جار$s</string>
|
||||
<string name="su_snack_log_on">تم تفعيل السجلات لـ %جار$s</string>
|
||||
<string name="su_snack_log_off">تم تعطيل السجلات لـ %جار$s</string>
|
||||
<string name="su_revoke_title">إلغاء؟</string>
|
||||
<string name="su_revoke_msg">تأكيد لإلغاء حقوق المستخدم الخارق لـ %جار$s</string>
|
||||
<string name="toast">إشعار</string>
|
||||
<string name="none">لا شيء</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="superuser_toggle_revoke">منع</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: %جار$d</string>
|
||||
<string name="target_uid">UID الهدف: %جار$d</string>
|
||||
<string name="target_pid">PID هدف النطاق المركب: %s</string>
|
||||
<string name="selinux_context">سياق SELinux: %s</string>
|
||||
<string name="supp_group">المجموعة التكميلية: %s</string>
|
||||
<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>
|
||||
|
||||
<!--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>
|
||||
<string name="show_system_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>
|
||||
<!--Module Fragment-->
|
||||
<string name="no_info_provided">(لا تتوفر معلومات)</string>
|
||||
<string name="reboot_recovery">إعادة التشغيل إلى Recovery</string>
|
||||
<string name="reboot_bootloader">إعادة التشغيل إلى Bootloader</string>
|
||||
<string name="reboot_download">إعادة التشغيل إلى وضـع Odin</string>
|
||||
<string name="reboot_edl">إعادة التشغيل إلى EDL</string>
|
||||
<string name="module_version_author">%1$sبواسطة%2$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">تم تعليق الإضافةة لأن %جار$s مفعّل</string>
|
||||
<string name="suspend_text_zygisk">تم تعليق الإضافةة لأن %جار$s غير مفعّل</string>
|
||||
<string name="zygisk_module_unloaded">لم يتم تحميل إضافة Zygisk بسبب عدم التوافق</string>
|
||||
<string name="module_empty">لم يتم تثبيت أي إضافة</string>
|
||||
<string name="confirm_install">هل تريد تثبيت الإضافة %جار$s؟</string>
|
||||
<string name="confirm_install_title">تأكيد التثبيت</string>
|
||||
<string name="module_state_restore">إسترجاع</string>
|
||||
<string name="module_action_install_external">اختر من الذاكرة الداخلية</string>
|
||||
<string name="update_available">التحديث متوفر</string>
|
||||
<string name="external_rw_permission_denied">امنحني إذن الولوج للذاكرة الداخلية</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">سيتم حفظ الملفات في %جار$s</string>
|
||||
<string name="settings_hide_app_title">إخفاء تطبيق Magisk</string>
|
||||
<string name="settings_hide_app_summary">تثبيت تطبيق وكيل بمعرف حزمة عشوائي واسم تطبيق مخصص</string>
|
||||
<string name="settings_restore_app_title">استعادة تطبيق Magisk</string>
|
||||
<string name="settings_restore_app_summary">إظهار التطبيق واستعادة APK الأصلي</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="language">اللغة</string>
|
||||
<string name="system_default">(افتراضي النظام)</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_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_beta">تجريبي</string>
|
||||
<string name="settings_update_custom">مخصص</string>
|
||||
<string name="settings_update_custom_msg">أدخل عنوان URL لقناة مخصصة</string>
|
||||
<string name="settings_zygisk_summary">تشغيل أجزاء من Magisk في خدمة zygote</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_update_custom_msg">أدخل الرابط لمصدرك المخصص</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>
|
||||
@@ -160,86 +136,55 @@
|
||||
<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">الحل لمشكلة تسميم DNS في بعض الدول</string>
|
||||
<string name="settings_random_name_title">تغيير الاسم عشوائيًا</string>
|
||||
<string name="settings_random_name_description">تغيير اسم ملف الخرج عشوائيًا لصور وملفات tar المصححة لمنع الاكتشاف</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_customization">تخصيص</string>
|
||||
|
||||
<string name="multiuser_mode">وضع المستخدمين المتعددين</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="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>
|
||||
<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">جميع الجلسات الروت تستخدم NameSpace العام</string>
|
||||
<string name="requester_summary">جميع الجلسات الروت تستخدم NameSpace المتوارث</string>
|
||||
<string name="isolate_summary">جميع الجلسات الروت تستخدم NameSpace المعزول</string>
|
||||
|
||||
<!--Notifications-->
|
||||
<string name="update_channel">تحديثات Magisk</string>
|
||||
<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">تحديث Magisk متاح!</string>
|
||||
<string name="updated_title">تم تحديث Magisk</string>
|
||||
<string name="updated_text">اضغط لفتح التطبيق</string>
|
||||
<string name="download_file_error">فشل تنزيل الملف</string>
|
||||
<string name="magisk_update_title">تحديث ماجيسك متوفر!</string>
|
||||
|
||||
<!--Toasts, Dialogs-->
|
||||
<string name="yes">نعم</string>
|
||||
<string name="no">لا</string>
|
||||
<string name="download">تنزيل</string>
|
||||
<string name="reboot">إعادة التشغيل</string>
|
||||
<string name="release_notes">ملاحظات الإصدار</string>
|
||||
<string name="flashing">جارٍ التثبيت...</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="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="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 الأقل من %جار$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" ثنائي ليس من 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>
|
||||
<string name="env_fix_title">الإعداد الإضافي مطلوب</string>
|
||||
<string name="setup_msg">جار إعداد البيئة</string>
|
||||
<string name="unsupport_magisk_title">إصدار ماجيسك غير مدعوم</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<string name="modules">Módulos</string>
|
||||
<string name="superuser">Superusuariu</string>
|
||||
<string name="logs">Rexistru</string>
|
||||
<string name="settings">Axustes</string>
|
||||
<string name="settings">Configuración</string>
|
||||
<string name="install">Instalar</string>
|
||||
<string name="section_home">Aniciu</string>
|
||||
<string name="section_theme">Estilos</string>
|
||||
@@ -15,7 +15,7 @@
|
||||
<string name="loading">Cargando…</string>
|
||||
<string name="update">Anovar</string>
|
||||
<string name="not_available">N/D</string>
|
||||
<string name="hide">Anubrir</string>
|
||||
<string name="hide">Esconder</string>
|
||||
<string name="home_package">Paquete</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>
|
||||
@@ -39,10 +39,10 @@
|
||||
<string name="manager_download_install">Primi equí pa baxalu ya instalalu</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_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="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="setup_title">Configuración adicional</string>
|
||||
<string name="select_patch_file">Seleicionar y parchiar un ficheru</string>
|
||||
<string name="patch_file_msg">Seleiciona una imaxe en bruto (*.img) o un archivu d\'ODIN (*.tar)</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="reboot_delay_toast">Reaniciando en 5 segundos…</string>
|
||||
<string name="flash_screen_title">Instalación</string>
|
||||
<!--Superuser-->
|
||||
@@ -81,6 +81,9 @@
|
||||
<string name="logs_cleared">El rexistru borróse correutamente</string>
|
||||
<string name="pid">PID: %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-->
|
||||
<!--MagiskHide-->
|
||||
<string name="show_system_app">Aplicaciones del sistema</string>
|
||||
@@ -94,15 +97,19 @@
|
||||
<string name="reboot_bootloader">Reaniciar al cargador d\'arrinque</string>
|
||||
<string name="reboot_download">Reaniciar al mou de descarga</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_state_remove">Quitar</string>
|
||||
<string name="module_action">Aición</string>
|
||||
<string name="module_state_restore">Restaurar</string>
|
||||
<string name="module_action_install_external">Instalar dende l\'almacenamientu</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_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ó pola mor d\'haber incompatibilidaes</string>
|
||||
<string name="zygisk_module_unloaded">El módulu de Zygisk nun cargó por haber incompatibilidaes</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-->
|
||||
<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>
|
||||
@@ -111,7 +118,7 @@
|
||||
<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_message">Los ficheros van guardase en «%1$s»</string>
|
||||
<string name="settings_hide_app_title">Anubrir Magisk</string>
|
||||
<string name="settings_hide_app_title">Esconder 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_restore_app_title">Restaurar el mou visible</string>
|
||||
<string name="settings_restore_app_summary">Fai que l\'aplicación orixinal vuelva ser visible</string>
|
||||
@@ -149,14 +156,19 @@
|
||||
<string name="auto_response">Rempuesta automática</string>
|
||||
<string name="request_timeout">Tiempu d\'espera de les solicitúes</string>
|
||||
<string name="superuser_notification">Avisu de superusuariu</string>
|
||||
<string name="settings_su_reauth_title">Volver autenticar darréu d\'anovar</string>
|
||||
<string name="settings_su_reauth_title">Volver autenticar dempués 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_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_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="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="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="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_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="settings_owner_only">Namás el propietariu del preséu</string>
|
||||
<string name="settings_owner_manage">El propietariu xestionáu del preséu</string>
|
||||
@@ -186,11 +198,14 @@
|
||||
<string name="repo_install_title">Instalación de: %1$s %2$s (%3$d)</string>
|
||||
<string name="download">Baxar</string>
|
||||
<string name="reboot">Reaniciar</string>
|
||||
<string name="close">Zarrar</string>
|
||||
<string name="release_notes">Notes de la versión</string>
|
||||
<string name="flashing">Flaxando…</string>
|
||||
<string name="running">Executando…</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="hide_app_title">Anubriendo l\'aplicación Magisk…</string>
|
||||
<string name="hide_app_title">Escondiendo l\'aplicación Magisk…</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="restore_img">Restaurar les imáxenes</string>
|
||||
@@ -200,6 +215,7 @@
|
||||
<string name="setup_fail">La configuración falló</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_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="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>
|
||||
@@ -207,12 +223,13 @@
|
||||
<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_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 anubrí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 escondí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="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="add_shortcut_title">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="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="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="restore_app_confirmation">Esta aición va restaurar l\'aplicación orixinal y desanicia la intermedia. ¿De xuru que quies facelo?</string>
|
||||
|
||||
250
app/core/src/main/res/values-b+sr+Latn/strings.xml
Normal file
250
app/core/src/main/res/values-b+sr+Latn/strings.xml
Normal file
@@ -0,0 +1,250 @@
|
||||
<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. Međutim, 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="restrict">Ograniči</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">Ciljani PID: %s</string>
|
||||
<string name="selinux_context">SELinux kontekst: %s</string>
|
||||
<string name="supp_group">Dopunska grupa: %s</string>
|
||||
|
||||
<!--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_debug">Debug</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_su_restrict_title">Ograniči korenske sposobnosti</string>
|
||||
<string name="settings_su_restrict_summary">Podrazumevano ograničava apl. super-korisnika. Upozorenje: ovo će većinu aplikacija skršiti. Ne omogućavaj, osim ako znaš šta radiš.</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>
|
||||
@@ -8,6 +8,7 @@
|
||||
<string name="install">نصب</string>
|
||||
<string name="section_home">خانه</string>
|
||||
<string name="section_theme">تم ها</string>
|
||||
<string name="denylist">لیست منع</string>
|
||||
|
||||
<!--Home-->
|
||||
<string name="no_connection">هیچ اتصالی وجود ندارد</string>
|
||||
@@ -17,7 +18,9 @@
|
||||
<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 را فقط از صفحه رسمی GitHub دانلود کنید. فایلها از منابع ناشناس میتوانند مخرب باشند!</string>
|
||||
<string name="home_follow_title">ما را دنبال کنید</string>
|
||||
<string name="home_support_title">حمایت ما</string>
|
||||
<string name="home_item_source">منبع</string>
|
||||
<string name="home_support_content">این برنامه (Magisk) رایگان و متن باز است و همیشه خواهد ماند. اگرچه شما میتواند با دونیت خود نشان دهد که به ما اهمیت می دهید.</string>
|
||||
@@ -47,8 +50,10 @@
|
||||
|
||||
<!--Superuser-->
|
||||
<string name="su_request_title">درخواست کاربر روت</string>
|
||||
<string name="touch_filtered_warning">به دلیل اینکه یک برنامه در حال پوشاندن درخواست Superuser است، Magisk نمیتواند پاسخ شما را تأیید کند.</string>
|
||||
<string name="deny">رد کردن</string>
|
||||
<string name="prompt">درخواست کردن</string>
|
||||
<string name="restrict">محدود کردن</string>
|
||||
<string name="grant">اجازه دادن</string>
|
||||
<string name="su_warning">دسترسی کامل به دستگاه شما را اعطا می کند. \nاگر مطمئن نیستید رد کنید!</string>
|
||||
<string name="forever">همیشه</string>
|
||||
@@ -67,37 +72,50 @@
|
||||
<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">لاگ های مربوط به Magisk خالی است. پنا بر خدا.</string>
|
||||
<string name="log_data_magisk_none">لاگ های مربوط به Magisk خالی است.</string>
|
||||
<string name="menuSaveLog">ذخیره کردن لاگ</string>
|
||||
<string name="menuClearLog">پاک کردن لاگ</string>
|
||||
<string name="logs_cleared">لاگ با موفقیت پاک شد.</string>
|
||||
|
||||
<!--SafetyNet-->
|
||||
<string name="pid">شناسه پردازش: %1$d</string>
|
||||
<string name="target_uid">شناسه کاربر هدف: %1$d</string>
|
||||
<string name="target_pid">شناسه پردازش هدف: %s</string>
|
||||
<string name="selinux_context">متن SELinux: %s</string>
|
||||
<string name="supp_group">گروه تکمیلی: %s</string>
|
||||
|
||||
<!-- 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 با %2$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>
|
||||
|
||||
|
||||
<!--Settings -->
|
||||
<string name="settings_dark_mode_title">حالت تم</string>
|
||||
@@ -107,6 +125,10 @@
|
||||
<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">نصب یک برنامه پروکسی با شناسه بسته تصادفی و برچسب سفارشی</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>
|
||||
@@ -114,8 +136,14 @@
|
||||
<string name="settings_update_channel_title">کانال بروزرسانی</string>
|
||||
<string name="settings_update_stable">پایدار</string>
|
||||
<string name="settings_update_beta">آزمایشی</string>
|
||||
<string name="settings_update_debug">اشکالزدایی</string>
|
||||
<string name="settings_update_custom">شخصی سازی شده</string>
|
||||
<string name="settings_update_custom_msg">اضافه کردن یک URL سفارشی</string>
|
||||
<string name="settings_zygisk_summary">اجرای بخشهایی از Magisk در سرویس Zygote</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">نصب بدون حذف یا تغییر در فایل ها رای ساپورت از برنامه های Adblock</string>
|
||||
<string name="settings_hosts_toast">ماژول نصب بدون حذف یا تغییر در فایل ها اضافه شد</string>
|
||||
@@ -138,9 +166,19 @@
|
||||
<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">پنجره درخواست Superuser زمانی که توسط پنجره یا لایه دیگری پوشانده شود، به ورودی پاسخ نخواهد داد</string>
|
||||
<string name="settings_su_auth_title">احراز هویت کاربر</string>
|
||||
<string name="settings_su_auth_summary">درخواست احراز هویت کاربر هنگام درخواست Superuser</string>
|
||||
<string name="settings_su_auth_insecure">هیچ روش احراز هویتی روی دستگاه پیکربندی نشده است</string>
|
||||
<string name="settings_su_restrict_title">محدود کردن دسترسی روت</string>
|
||||
<string name="settings_su_restrict_summary">به طور پیشفرض برنامههای Superuser جدید را محدود میکند. هشدار: این کار بیشتر برنامهها را از کار میاندازد. فقط اگر دقیقاً میدانید چه میکنید آن را فعال کنید.</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>
|
||||
@@ -148,7 +186,6 @@
|
||||
<string name="owner_only_summary">فقط مالک دسترسی روت دارد</string>
|
||||
<string name="owner_manage_summary">فقط مالک می تواند دسترسی روت را مدیریت کرده و درخواست های پرامپت را دریافت کند</string>
|
||||
<string name="user_independent_summary">هر کاربر قوانین روت جداگانه خود را دارد</string>
|
||||
|
||||
<string name="mount_namespace_mode">نصب کردن Namespace Mode</string>
|
||||
<string name="settings_ns_global">سراسری Namespace</string>
|
||||
<string name="settings_ns_requester">وراثتی Namespace</string>
|
||||
@@ -160,19 +197,27 @@
|
||||
<!--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="download">انلود کردن</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">ناموفق</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>
|
||||
@@ -181,9 +226,24 @@
|
||||
<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 را از داخل برنامه دوباره نصب کنید، حالت Recovery نمیتواند اطلاعات دستگاه را به درستی بگیرد.</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" غیر از 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>
|
||||
|
||||
249
app/core/src/main/res/values-hn/strings.xml
Normal file
249
app/core/src/main/res/values-hn/strings.xml
Normal file
@@ -0,0 +1,249 @@
|
||||
<resources>
|
||||
|
||||
<!--Sections-->
|
||||
<string name="modules">Modules</string>
|
||||
<string name="superuser">Superuser</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="install">Install</string>
|
||||
<string name="section_home">Home</string>
|
||||
<string name="section_theme">Themes</string>
|
||||
<string name="denylist">DenyList</string>
|
||||
|
||||
<!--Home-->
|
||||
<string name="no_connection">Net nahi chal rha hai</string>
|
||||
<string name="app_changelog">Kya naga hai</string>
|
||||
<string name="loading">Loading ho rha hai…</string>
|
||||
<string name="update">Update</string>
|
||||
<string name="not_available">Available nahi hai</string>
|
||||
<string name="hide">Chhupao</string>
|
||||
<string name="home_package">Package</string>
|
||||
<string name="home_app_title">App</string>
|
||||
<string name="home_notice_content">Hamesha Magisk ko uske official github release source se download karein. Unofficial source ki file khatarnak ho sakti hai.</string>
|
||||
<string name="home_support_title">Humein Support karo</string>
|
||||
<string name="home_follow_title">Humein Karo</string>
|
||||
<string name="home_item_source">Source</string>
|
||||
<string name="home_support_content">Magisk hamesha free aur open source rahega. Agar aap support karna chahte ho, toh donation de sakte ho.</string>
|
||||
<string name="home_installed_version">Jo version install hai</string>
|
||||
<string name="home_latest_version">Latest</string>
|
||||
<string name="invalid_update_channel">Galat update channel</string>
|
||||
<string name="uninstall_magisk_title">Magisk uninstall karo</string>
|
||||
<string name="uninstall_magisk_msg">Saare modules disable ya remove ho jayenge!\nRoot khatam ho jayega!\nAgar Magisk ne storage ko decrypt kiya tha toh wo phir se encrypt ho jayega!</string>
|
||||
|
||||
<!--Install-->
|
||||
<string name="keep_force_encryption">Force encryption ko preserve karo</string>
|
||||
<string name="keep_dm_verity">AVB 2.0/dm-verity ko preserve karo</string>
|
||||
<string name="recovery_mode">Recovery mode</string>
|
||||
<string name="install_options_title">Options</string>
|
||||
<string name="install_method_title">Method</string>
|
||||
<string name="install_next">Aage badho</string>
|
||||
<string name="install_start">Chalo shuru karein</string>
|
||||
<string name="manager_download_install">Download aur install karne ke liye dabao</string>
|
||||
<string name="direct_install">Direct install (Recommended)</string>
|
||||
<string name="install_inactive_slot">Inactive slot par install karo (OTA ke baad)</string>
|
||||
<string name="install_inactive_slot_msg">Reboot ke baad device ko inactive slot se boot karna padega!\nSirf tab use karo jab OTA complete ho chuka ho.\nAage badhna hai?</string>
|
||||
<string name="setup_title">Extra setup</string>
|
||||
<string name="select_patch_file">File select karke patch karo</string>
|
||||
<string name="patch_file_msg">Raw image (*.img), ODIN tar file (*.tar), ya payload.bin (*.bin) select karo</string>
|
||||
<string name="reboot_delay_toast">Phone 5 second mein reboot hoga…</string>
|
||||
<string name="flash_screen_title">Installation ho rahi hai</string>
|
||||
|
||||
<!--Superuser-->
|
||||
<string name="su_request_title">ROOT access maang rha hai</string>
|
||||
<string name="touch_filtered_warning">Ek app Superuser request ko cover kar raha hai, isliye Magisk aapka response verify nahi kar sakta.</string>
|
||||
<string name="deny">Nahi</string>
|
||||
<string name="prompt">Poocho</string>
|
||||
<string name="restrict">Restrict</string>
|
||||
<string name="grant">Haan Theek hai</string>
|
||||
<string name="su_warning">Yeh full access dega device ko.\nAgar sure nahi ho toh deny kar do!</string>
|
||||
<string name="forever">Hamesha ke liye</string>
|
||||
<string name="once">Ek baar ke liye</string>
|
||||
<string name="tenmin">10 mins</string>
|
||||
<string name="twentymin">20 mins</string>
|
||||
<string name="thirtymin">30 mins</string>
|
||||
<string name="sixtymin">60 mins</string>
|
||||
<string name="su_allow_toast">%1$s ko ROOT access de diya gaya</string>
|
||||
<string name="su_deny_toast">%1$s ko ROOT access nahi diya gaya</string>
|
||||
<string name="su_snack_grant">%1$s ko ROOT access mila</string>
|
||||
<string name="su_snack_deny">%1$s ka ROOT access deny hua</string>
|
||||
<string name="su_snack_notif_on">%1$s ke notifications ON hain</string>
|
||||
<string name="su_snack_notif_off">%1$s ke notifications OFF hain</string>
|
||||
<string name="su_snack_log_on">%1$s ka logging ON hai</string>
|
||||
<string name="su_snack_log_off">%1$s ka logging OFF hai</string>
|
||||
<string name="su_revoke_title">Access wapas lena hai?</string>
|
||||
<string name="su_revoke_msg">Kya aap confirm karte ho ki %1$s ka ROOT access hata diya jaye?</string>
|
||||
<string name="toast">Toast</string>
|
||||
<string name="none">Kuch nahi</string>
|
||||
<string name="superuser_toggle_notification">Notifications</string>
|
||||
<string name="superuser_toggle_revoke">Wapas lelo</string>
|
||||
<string name="superuser_policy_none">Ab tak kisi bhi app ne ROOT access nahi manga hai</string>
|
||||
|
||||
<!--Logs-->
|
||||
<string name="log_data_none">Koi logs nahi mile. Shayad tumhe root apps ka zyada use krna chahiye.</string>
|
||||
<string name="log_data_magisk_none">Ajeeb baat hai, Yaha toh logs hain hi nahi.</string>
|
||||
<string name="menuSaveLog">Log save karo</string>
|
||||
<string name="menuClearLog">Log clear karo</string>
|
||||
<string name="logs_cleared">Logs clear ho gaye</string>
|
||||
<string name="pid">PID: %1$d</string>
|
||||
<string name="target_uid">Target UID: %1$d</string>
|
||||
<string name="target_pid">Target PID: %s</string>
|
||||
<string name="selinux_context">SELinux context: %s</string>
|
||||
<string name="supp_group">Supplementary group: %s</string>
|
||||
|
||||
<!--MagiskHide-->
|
||||
<string name="show_system_app">System apps dikhao</string>
|
||||
<string name="show_os_app">OS apps dikhao</string>
|
||||
<string name="hide_filter_hint">Naam ke hisaab se filter karo</string>
|
||||
<string name="hide_search">Search karo</string>
|
||||
|
||||
<!--Module-->
|
||||
<string name="no_info_provided">(Koi info nahi di gayi)</string>
|
||||
<string name="reboot_userspace">Soft reboot</string>
|
||||
<string name="reboot_recovery">Recovery mode mein reboot karo</string>
|
||||
<string name="reboot_bootloader">Bootloader mode mein reboot karo</string>
|
||||
<string name="reboot_download">Download mode mein reboot karo</string>
|
||||
<string name="reboot_edl">EDL mode mein reboot karo</string>
|
||||
<string name="reboot_safe_mode">Safe mode</string>
|
||||
<string name="module_version_author">%1$s ko %2$s ne banaya hai</string>
|
||||
<string name="module_state_remove">Delete karo</string>
|
||||
<string name="module_action">Action</string>
|
||||
<string name="module_state_restore">Wapas laao</string>
|
||||
<string name="module_action_install_external">Storage se install karo</string>
|
||||
<string name="update_available">Naya update available hai</string>
|
||||
<string name="suspend_text_riru">Module suspend kiya gaya kyunki %1$s enabled hai</string>
|
||||
<string name="suspend_text_zygisk">Bhai %1$s ON kr tabb ye module chalega</string>
|
||||
<string name="zygisk_module_unloaded">Zygisk module compatible nahi tha, isliye load nahi hua</string>
|
||||
<string name="module_empty">Koi module install nahi hai</string>
|
||||
<string name="confirm_install">>%1$s module install karna hai?</string>
|
||||
<string name="confirm_install_title">Ek baar firse soch lo</string>
|
||||
|
||||
<!--Settings-->
|
||||
<string name="settings_dark_mode_title">Theme mode</string>
|
||||
<string name="settings_dark_mode_message">Apni hisaab se mode choose karo!</string>
|
||||
<string name="settings_dark_mode_light">Hamesha light theme</string>
|
||||
<string name="settings_dark_mode_system">System ke saath follow karo</string>
|
||||
<string name="settings_dark_mode_dark">Hamesha dark theme</string>
|
||||
<string name="settings_download_path_title">Download path</string>
|
||||
<string name="settings_download_path_message">Files yahan save hongi: %1$s</string>
|
||||
<string name="settings_hide_app_title">Magisk app ko hide karo</string>
|
||||
<string name="settings_hide_app_summary">Magisk app ka name aur package ID change kro</string>
|
||||
<string name="settings_restore_app_title">Magisk app ko unhide karo</string>
|
||||
<string name="settings_restore_app_summary">App ko unhide karo aur original APK wapas lao</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="system_default">(System ki default)</string>
|
||||
<string name="settings_check_update_title">Updates check karo</string>
|
||||
<string name="settings_check_update_summary">Background mein updates auto check honge</string>
|
||||
<string name="settings_update_channel_title">Update channel</string>
|
||||
<string name="settings_update_stable">Stable</string>
|
||||
<string name="settings_update_beta">Beta</string>
|
||||
<string name="settings_update_debug">Debug</string>
|
||||
<string name="settings_update_custom">Custom</string>
|
||||
<string name="settings_update_custom_msg">Apna custom channel URL daaloL</string>
|
||||
<string name="settings_zygisk_summary">Magisk ke kuch parts ko Zygote daemon mein run karo</string>
|
||||
<string name="settings_denylist_title">DenyList enforce karo</string>
|
||||
<string name="settings_denylist_summary">DenyList mein jo processes hain, unpe Magisk ka effect hata diya jayega</string>
|
||||
<string name="settings_denylist_config_title">DenyList set karo</string>
|
||||
<string name="settings_denylist_config_summary">Kaunse process DenyList mein daalne hain, select karo</string>
|
||||
<string name="settings_hosts_title">Systemless hosts</string>
|
||||
<string name="settings_hosts_summary">Ad-blocking apps ke liye systemless hosts support</string>
|
||||
<string name="settings_hosts_toast">Systemless hosts module add kar diya gaya</string>
|
||||
<string name="settings_app_name_hint">Naya naam</string>
|
||||
<string name="settings_app_name_helper">App is naam ke saath repack hoga</string>
|
||||
<string name="settings_app_name_error">Naam ka format galat hai</string>
|
||||
<string name="settings_su_app_adb">Apps aur ADB</string>
|
||||
<string name="settings_su_app">Sirf apps</string>
|
||||
<string name="settings_su_adb">Sirf ADB</string>
|
||||
<string name="settings_su_disable">Disable kiya gaya</string>
|
||||
<string name="settings_su_request_10">10 seconds</string>
|
||||
<string name="settings_su_request_15">15 seconds</string>
|
||||
<string name="settings_su_request_20">20 seconds</string>
|
||||
<string name="settings_su_request_30">30 seconds</string>
|
||||
<string name="settings_su_request_45">45 seconds</string>
|
||||
<string name="settings_su_request_60">60 seconds</string>
|
||||
<string name="superuser_access">ROOT access</string>
|
||||
<string name="auto_response">Automatic response</string>
|
||||
<string name="request_timeout">Request timeout</string>
|
||||
<string name="superuser_notification">ROOT notification</string>
|
||||
<string name="settings_su_reauth_title">Upgrade ke baad dobara permission puchho</string>
|
||||
<string name="settings_su_reauth_summary">App upgrade hone ke baad Superuser permission firse maangna</string>
|
||||
<string name="settings_su_tapjack_title">Tapjacking se protection</string>
|
||||
<string name="settings_su_tapjack_summary">Agar Superuser prompt kisi aur window ya overlay ke neeche chhup gaya ho, toh uspar tap kaam nahi karega</string>
|
||||
<string name="settings_su_auth_title">User authentication</string>
|
||||
<string name="settings_su_auth_summary">Superuser request ke time user se authentication maango</string>
|
||||
<string name="settings_su_auth_insecure">Device mein koi bhi authentication method set nahi hai</string>
|
||||
<string name="settings_su_restrict_title">Root access ko limit karo</string>
|
||||
<string name="settings_su_restrict_summary">Nayeapps ko ROOT access maangne se block karega. Warning: Isse zyada tarr apps kaam karna band kar denge. Sirf tab enable karo jab aapko pata ho aap kya kar rahe ho.</string>
|
||||
<string name="settings_customization">Customization</string>
|
||||
<string name="setting_add_shortcut_summary">Agar app hide karne ke baad uska naam ya icon samajhne mein dikkat ho rahi ho, toh home screen pe ek shortcut add kar lo</string>
|
||||
<string name="settings_doh_title">DNS over HTTPS</string>
|
||||
<string name="settings_doh_description">DNS poisoning se bachne ke liye (kuch countries mein zaroori padti hai)</string>
|
||||
<string name="settings_random_name_title">Output file ka naam random karo</string>
|
||||
<string name="settings_random_name_description">Patched images aur tar files ka naam random bana ke detection se bachao</string>
|
||||
<string name="multiuser_mode">Multiuser mode</string>
|
||||
<string name="settings_owner_only">Sirf device owner</string>
|
||||
<string name="settings_owner_manage">Device owner manage karega</string>
|
||||
<string name="settings_user_independent">Har user ke liye alag</string>
|
||||
<string name="owner_only_summary">Sirf device owner ko root access milega</string>
|
||||
<string name="owner_manage_summary">Sirf owner root access manage kar sakta hai aur requests receive karega</string>
|
||||
<string name="user_independent_summary">Har user ke liye alag root rules honge</string>
|
||||
<string name="mount_namespace_mode">Mount namespace mode</string>
|
||||
<string name="settings_ns_global">Global namespace</string>
|
||||
<string name="settings_ns_requester">Inherit namespace</string>
|
||||
<string name="settings_ns_isolate">Isolated namespace</string>
|
||||
<string name="global_summary">Sabhi root sessions ek hi global mount namespace use karenge</string>
|
||||
<string name="requester_summary">Root session apne requester ka namespace inherit karega</string>
|
||||
<string name="isolate_summary">Har root session ka apna alag isolated namespace hoga</string>
|
||||
|
||||
<!--Notifications-->
|
||||
<string name="update_channel">Magisk updates</string>
|
||||
<string name="progress_channel">Progress notifications</string>
|
||||
<string name="updated_channel">Update ho gaya</string>
|
||||
<string name="download_complete">Download ho gaya</string>
|
||||
<string name="download_file_error">File download karne mein error aaya</string>
|
||||
<string name="magisk_update_title">Magisk ka naya update available hai!</string>
|
||||
<string name="updated_title">Magisk update ho gaya</string>
|
||||
<string name="updated_text">App open karne ke liye tap karo</string>
|
||||
|
||||
<!--Toasts, Dialogs-->
|
||||
<string name="yes">Haan</string>
|
||||
<string name="no">Nahi</string>
|
||||
<string name="repo_install_title">%1$s %2$s(%3$d) Install karo</string>
|
||||
<string name="download">Download karo</string>
|
||||
<string name="reboot">Reboot karo</string>
|
||||
<string name="close">Close karo</string>
|
||||
<string name="release_notes">Release notes</string>
|
||||
<string name="flashing">Flash ho raha hai…</string>
|
||||
<string name="running">Chal raha hai…</string>
|
||||
<string name="done">Ho gaya!</string>
|
||||
<string name="done_action">%1$s ka action complete ho gaya</string>
|
||||
<string name="failure">Fail ho gaya!</string>
|
||||
<string name="hide_app_title">Magisk app ko chhupa rahe hain…</string>
|
||||
<string name="open_link_failed_toast">Link kholne ke liye koi app nahi mila</string>
|
||||
<string name="complete_uninstall">Sab kuch uninstall karo</string>
|
||||
<string name="restore_img">Images restore karo</string>
|
||||
<string name="restore_img_msg">Restore kiya ja raha hai…</string>
|
||||
<string name="restore_done">Restore complete ho gaya!</string>
|
||||
<string name="restore_fail">Stock backup available nahi hai!</string>
|
||||
<string name="setup_fail">Setup fail ho gaya</string>
|
||||
<string name="env_fix_title">Iske liye thoda extra setup chahiye</string>
|
||||
<string name="env_fix_msg">Magisk ko sahi se chalane ke liye thoda aur setup karna padega. Kya aap proceed karke device reboot karna chahte ho?</string>
|
||||
<string name="env_full_fix_msg">Magisk ko properly chalane ke liye aapko usse dubara flash karna padega. App ke andar se Magisk reinstall karo, kyunki Recovery mode sahi device info nahi de pata.</string>
|
||||
<string name="setup_msg">Environment setup chal raha hai…</string>
|
||||
<string name="unsupport_magisk_title">Magisk version supported nahi hain</string>
|
||||
<string name="unsupport_magisk_msg">Is app version ko %1$s se purani Magisk versions support nahi karti.\n\nApp aise behave karega jaise Magisk install hi nahi hai. Jaldi se Magisk ko update karo.</string>
|
||||
<string name="unsupport_general_title">System thoda alag behave kar raha hai</string>
|
||||
<string name="unsupport_system_app_msg">App ko system app bana ke chalana supported nahi hai. Please app ko wapas user app bana do.</string>
|
||||
<string name="unsupport_other_su_msg">Pehle doosri root method hatao ya Magisk dobara install karo.</string>
|
||||
<string name="unsupport_external_storage_msg">Magisk external storage pe installed hai. App ko internal storage mein move karo.</string>
|
||||
<string name="unsupport_nonroot_stub_msg">Hidden Magisk app ab kaam nahi karega kyunki root chala gaya hai. Please original APK restore karo.</string>
|
||||
<string name="unsupport_nonroot_stub_title">@string/settings_restore_app_title</string>
|
||||
<string name="external_rw_permission_denied">Iss feature ko enable karne ke liye storage permission allow karo</string>
|
||||
<string name="post_notifications_denied">Is feature ke liye notifications permission allow karo</string>
|
||||
<string name="install_unknown_denied">\"Install unknown apps\" ki permission allow karo taaki ye feature kaam kar sakey</string>
|
||||
<string name="add_shortcut_title">Shortcut home screen pe add karo</string>
|
||||
<string name="add_shortcut_msg">App ko hide karne ke baad agar uska icon ya naam pehchanna mushkil ho, toh ek shortcut home screen pe add kar lein?</string>
|
||||
<string name="app_not_found">Is action ko handle karne ke liye koi app nahi mila</string>
|
||||
<string name="reboot_apply_change">Changes apply karne ke liye reboot krna zaroori hai</string>
|
||||
<string name="restore_app_confirmation">Ye action hidden app ko original version mein wapas laayega. Kya aap sach mein ye karna chahte ho?</string>
|
||||
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user