mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 09:57:29 +00:00
Compare commits
368 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7cb0909c70 | ||
![]() |
cc5ff36165 | ||
![]() |
18b1ef6c29 | ||
![]() |
7fe012347a | ||
![]() |
5c165c9bb0 | ||
![]() |
6c3519923d | ||
![]() |
9ea859810d | ||
![]() |
8dae7b5451 | ||
![]() |
f827755aaf | ||
![]() |
637a8af234 | ||
![]() |
b0fc580860 | ||
![]() |
9279f30e89 | ||
![]() |
b505819ca2 | ||
![]() |
39d1d23909 | ||
![]() |
69529ac59c | ||
![]() |
a18a440236 | ||
![]() |
aa7846c1c0 | ||
![]() |
24ba4ab95b | ||
![]() |
762b70ba9d | ||
![]() |
41b77e4f25 | ||
![]() |
2087e47300 | ||
![]() |
46ce765860 | ||
![]() |
5117dc1a31 | ||
![]() |
620fd7d124 | ||
![]() |
3e991dc003 | ||
![]() |
15cab86152 | ||
![]() |
aa785b5845 | ||
![]() |
97731a519a | ||
![]() |
b696dae808 | ||
![]() |
732a8260c2 | ||
![]() |
4ff60ef9a9 | ||
![]() |
23b1b69110 | ||
![]() |
3a4fe53f27 | ||
![]() |
e48afff5e8 | ||
![]() |
3f4f4598e8 | ||
![]() |
3921e9cb1b | ||
![]() |
0b987dd0b0 | ||
![]() |
1620e15f99 | ||
![]() |
b089511e91 | ||
![]() |
958788c1aa | ||
![]() |
b5a8a27296 | ||
![]() |
98123775ad | ||
![]() |
c7133974be | ||
![]() |
04324a7ebe | ||
![]() |
f54daa3469 | ||
![]() |
07c22ccd39 | ||
![]() |
e893c13cf1 | ||
![]() |
dba5020e4f | ||
![]() |
87e036a190 | ||
![]() |
3dd94672b0 | ||
![]() |
004b193f69 | ||
![]() |
4417997749 | ||
![]() |
2eef542054 | ||
![]() |
a07d4080b6 | ||
![]() |
b9d0a3b3d4 | ||
![]() |
76405bd984 | ||
![]() |
4e2b88b3d0 | ||
![]() |
7048aa1014 | ||
![]() |
1c2fcd14b5 | ||
![]() |
84e1bd7bc3 | ||
![]() |
362eea741f | ||
![]() |
4de93cfd4b | ||
![]() |
03cee0b8d4 | ||
![]() |
54ecc001f4 | ||
![]() |
5c325d9466 | ||
![]() |
0e851cdcf8 | ||
![]() |
af054e4e31 | ||
![]() |
33fb4653f0 | ||
![]() |
d9f0aed571 | ||
![]() |
98813c24fb | ||
![]() |
3cc81bb3fd | ||
![]() |
366dd52419 | ||
![]() |
fe6b658c02 | ||
![]() |
3cf66d1c57 | ||
![]() |
382568bd3c | ||
![]() |
d130aa02a1 | ||
![]() |
1a1646795f | ||
![]() |
d52ea1b068 | ||
![]() |
e14f7b6908 | ||
![]() |
4709a32641 | ||
![]() |
71b7f52663 | ||
![]() |
981ccabbef | ||
![]() |
9e07eb592c | ||
![]() |
9555380818 | ||
![]() |
f80d5d858e | ||
![]() |
a1ce6f5f12 | ||
![]() |
1aade8f8a8 | ||
![]() |
b9213b7043 | ||
![]() |
4af72324f4 | ||
![]() |
b6ea5b8984 | ||
![]() |
c279e08c88 | ||
![]() |
2717feac21 | ||
![]() |
8adf27859d | ||
![]() |
307cf87215 | ||
![]() |
ca31412c05 | ||
![]() |
f59fbd5dca | ||
![]() |
2285f5e888 | ||
![]() |
da36e5bcd5 | ||
![]() |
4ed9f57fdc | ||
![]() |
ea7be6162f | ||
![]() |
3726eb6032 | ||
![]() |
6e918ffd68 | ||
![]() |
4772868d6a | ||
![]() |
78df677a42 | ||
![]() |
85a4b249b3 | ||
![]() |
d06e9a0b51 | ||
![]() |
5eb774a2ad | ||
![]() |
cbbbbab483 | ||
![]() |
e5641d5bdb | ||
![]() |
a721206c6f | ||
![]() |
c7a27481f9 | ||
![]() |
594c304634 | ||
![]() |
d0ec387c28 | ||
![]() |
7dbfba76bf | ||
![]() |
2a4aa95a6f | ||
![]() |
5520f0fbf7 | ||
![]() |
a1a87c9956 | ||
![]() |
2c53356bfd | ||
![]() |
85d9756f62 | ||
![]() |
79586ece4c | ||
![]() |
6851d11a8e | ||
![]() |
996a857096 | ||
![]() |
d7158131e4 | ||
![]() |
3d3082bc82 | ||
![]() |
744ebca206 | ||
![]() |
92077ebe53 | ||
![]() |
78ca682bc5 | ||
![]() |
af01a36296 | ||
![]() |
97ed1b16d0 | ||
![]() |
508a001753 | ||
![]() |
c1909d520b | ||
![]() |
9b1e173373 | ||
![]() |
4ba365565f | ||
![]() |
ae34659b26 | ||
![]() |
79a85f5937 | ||
![]() |
b249832571 | ||
![]() |
577b5912af | ||
![]() |
9e8c68af12 | ||
![]() |
03418ddcbf | ||
![]() |
220a1c84ce | ||
![]() |
9a4458ffac | ||
![]() |
7a9e6d2ad2 | ||
![]() |
9656cf2f86 | ||
![]() |
584bad5314 | ||
![]() |
459088024f | ||
![]() |
d740bbe058 | ||
![]() |
6ecc04a4df | ||
![]() |
15a7e9af57 | ||
![]() |
0329f00129 | ||
![]() |
cd8a2edefb | ||
![]() |
4318ab5cd2 | ||
![]() |
3517e6d752 | ||
![]() |
67845f9c21 | ||
![]() |
f562710438 | ||
![]() |
e836909c50 | ||
![]() |
7769ba5f54 | ||
![]() |
7fe9db90a1 | ||
![]() |
8f7d6dfb77 | ||
![]() |
2839978cc1 | ||
![]() |
e73f87b758 | ||
![]() |
bd0409fd15 | ||
![]() |
babdfe80cb | ||
![]() |
636223b289 | ||
![]() |
aa0a2f77cf | ||
![]() |
e38f35eab2 | ||
![]() |
cb39514705 | ||
![]() |
78a444d601 | ||
![]() |
37b81ad1f6 | ||
![]() |
7871c2f595 | ||
![]() |
57d83635c6 | ||
![]() |
76fbf4634a | ||
![]() |
7ce4bd3330 | ||
![]() |
ad0e6511e1 | ||
![]() |
a4a734458b | ||
![]() |
f989756b93 | ||
![]() |
5763a3d908 | ||
![]() |
1b745ae1a0 | ||
![]() |
b6d50bea2c | ||
![]() |
831a398bf1 | ||
![]() |
a848783b97 | ||
![]() |
4d876f0145 | ||
![]() |
bdfedea4e0 | ||
![]() |
ea0e3a09ef | ||
![]() |
dadae20960 | ||
![]() |
4ed34cd648 | ||
![]() |
0d38c94c9c | ||
![]() |
2a2a452bd4 | ||
![]() |
13c2695e98 | ||
![]() |
3ff60ed49f | ||
![]() |
bbb1786ec3 | ||
![]() |
4bfd2dac54 | ||
![]() |
857c12372a | ||
![]() |
33f5154269 | ||
![]() |
ed37ddd570 | ||
![]() |
cd5384f13e | ||
![]() |
11b2ddbad8 | ||
![]() |
cf9957ce4d | ||
![]() |
44643ad7b3 | ||
![]() |
1e53a5555e | ||
![]() |
616adc22e1 | ||
![]() |
916e373edb | ||
![]() |
021ae15395 | ||
![]() |
52cf72002a | ||
![]() |
68874bf571 | ||
![]() |
a468fd946d | ||
![]() |
e327565434 | ||
![]() |
c3b4678f6e | ||
![]() |
978216eade | ||
![]() |
44cfe94e4d | ||
![]() |
f9e82c9e8a | ||
![]() |
25b4b107d3 | ||
![]() |
db651fa9ec | ||
![]() |
23ad611566 | ||
![]() |
095d821240 | ||
![]() |
e23f23a8b7 | ||
![]() |
48f829b76e | ||
![]() |
0b82fe197c | ||
![]() |
af99c1b843 | ||
![]() |
c6646efe68 | ||
![]() |
66a7ef5615 | ||
![]() |
9474750bdf | ||
![]() |
e86db0bd61 | ||
![]() |
a29fc11798 | ||
![]() |
a66a3b7438 | ||
![]() |
44029875a6 | ||
![]() |
ccf21b0992 | ||
![]() |
4e14dab60a | ||
![]() |
6e299018a4 | ||
![]() |
555a54ec53 | ||
![]() |
1565bf5442 | ||
![]() |
14b830027b | ||
![]() |
38325e708e | ||
![]() |
646260ad6d | ||
![]() |
d1d26f4481 | ||
![]() |
357d913f18 | ||
![]() |
71b0c8b42b | ||
![]() |
cdc66c1ac8 | ||
![]() |
e9af773901 | ||
![]() |
eadf6e8b96 | ||
![]() |
87bec70d9f | ||
![]() |
3668b28f62 | ||
![]() |
933e4bd163 | ||
![]() |
e3ab9e9a1e | ||
![]() |
58ad2c1416 | ||
![]() |
c5291ad33b | ||
![]() |
77d8445bfd | ||
![]() |
f8395a7dc6 | ||
![]() |
727c70005e | ||
![]() |
38ab6858f0 | ||
![]() |
a54114f149 | ||
![]() |
7a4a5c8992 | ||
![]() |
928a16d8cc | ||
![]() |
3f7f6e619a | ||
![]() |
c2f96975ce | ||
![]() |
8bd4760b00 | ||
![]() |
4f4aeb893d | ||
![]() |
fed4f1b50f | ||
![]() |
e11087cd1a | ||
![]() |
e6eb51551c | ||
![]() |
c5c608f0d3 | ||
![]() |
4737c5117a | ||
![]() |
9806b38d8e | ||
![]() |
6bfe34e5a8 | ||
![]() |
34dd9eb7d6 | ||
![]() |
2d8beabbd4 | ||
![]() |
4d9b7e7114 | ||
![]() |
40aab13601 | ||
![]() |
4c0f72f68f | ||
![]() |
dd565a11ea | ||
![]() |
1735a713cb | ||
![]() |
52ba6d11bc | ||
![]() |
7357a35f8d | ||
![]() |
aeb7fd7cb3 | ||
![]() |
1b4a6850b8 | ||
![]() |
07b45f39df | ||
![]() |
1d0b873950 | ||
![]() |
d449f49d73 | ||
![]() |
e8787b5cfd | ||
![]() |
d17ed2b979 | ||
![]() |
b496923cbb | ||
![]() |
759d196aad | ||
![]() |
a7ab8216ce | ||
![]() |
b9e89a1a2d | ||
![]() |
c7c9fb9576 | ||
![]() |
8b095de04d | ||
![]() |
468325b51a | ||
![]() |
e5058bfb8b | ||
![]() |
d4b9ef736d | ||
![]() |
00d3cb0908 | ||
![]() |
d35072d4e6 | ||
![]() |
1a964e78dd | ||
![]() |
4264ae49c0 | ||
![]() |
f08712cd0a | ||
![]() |
3906fe75dc | ||
![]() |
2497e548c9 | ||
![]() |
e4635684e9 | ||
![]() |
9b61bdfc9a | ||
![]() |
6066b5cf86 | ||
![]() |
5cdf95a4d0 | ||
![]() |
910a36fdc1 | ||
![]() |
8331206acb | ||
![]() |
8423dc8d63 | ||
![]() |
6077c989a7 | ||
![]() |
c97d1044fa | ||
![]() |
f42c089b26 | ||
![]() |
1f8c063dc6 | ||
![]() |
4874520d65 | ||
![]() |
5e53639969 | ||
![]() |
83ab0ca6cd | ||
![]() |
70fd03d5fc | ||
![]() |
2e52875b50 | ||
![]() |
fd9b990ad7 | ||
![]() |
69978a9442 | ||
![]() |
d155da52ce | ||
![]() |
9c5b131913 | ||
![]() |
9d740cec1a | ||
![]() |
c2978eb9c3 | ||
![]() |
38abad1e44 | ||
![]() |
b4863eb51b | ||
![]() |
3817167ba1 | ||
![]() |
d1a35dd2ba | ||
![]() |
26116ac414 | ||
![]() |
0b26882fce | ||
![]() |
a2495fb5fb | ||
![]() |
0beb3bf16a | ||
![]() |
b68658e974 | ||
![]() |
3ae7344747 | ||
![]() |
4eb71830b3 | ||
![]() |
9183a0a6ea | ||
![]() |
bb64ba0ef6 | ||
![]() |
d89a568897 | ||
![]() |
9fd1f41e8b | ||
![]() |
c1ab348673 | ||
![]() |
00247c7901 | ||
![]() |
3c75f474c6 | ||
![]() |
db1f5b0397 | ||
![]() |
db277c3e55 | ||
![]() |
b9c93c66f6 | ||
![]() |
a250e2b56c | ||
![]() |
cd96454886 | ||
![]() |
741b679306 | ||
![]() |
90013e486d | ||
![]() |
4e2ecdb920 | ||
![]() |
6e5df1f06b | ||
![]() |
9469e79e3c | ||
![]() |
db78c20161 | ||
![]() |
1699da1754 | ||
![]() |
754e690274 | ||
![]() |
6f74ed6ceb | ||
![]() |
71205bc530 | ||
![]() |
10e236abdf | ||
![]() |
2248af00f3 | ||
![]() |
7e61716277 | ||
![]() |
50edb8d072 | ||
![]() |
515f81944c | ||
![]() |
46d4708386 | ||
![]() |
aabc36f86b | ||
![]() |
e0d5d90267 | ||
![]() |
482a5b991b | ||
![]() |
20124fe410 | ||
![]() |
f8dcec116a | ||
![]() |
343a339aae | ||
![]() |
42606efe56 | ||
![]() |
cae58c8790 | ||
![]() |
3a39dd4049 | ||
![]() |
89ff3c6572 | ||
![]() |
7bf9c74216 | ||
![]() |
e2f3753551 |
19
.github/ccache.sh
vendored
19
.github/ccache.sh
vendored
@@ -1,19 +0,0 @@
|
||||
OS=$(uname)
|
||||
CCACHE_VER=4.4
|
||||
|
||||
case $OS in
|
||||
Darwin )
|
||||
brew install ccache
|
||||
ln -s $(which ccache) ./ccache
|
||||
;;
|
||||
Linux )
|
||||
sudo apt-get install -y ccache
|
||||
ln -s $(which ccache) ./ccache
|
||||
;;
|
||||
* )
|
||||
curl -OL https://github.com/ccache/ccache/releases/download/v${CCACHE_VER}/ccache-${CCACHE_VER}-windows-64.zip
|
||||
unzip -j ccache-*-windows-64.zip '*/ccache.exe'
|
||||
;;
|
||||
esac
|
||||
mkdir ./.ccache
|
||||
./ccache -o compiler_check='%compiler% -dumpmachine; %compiler% -dumpversion'
|
43
.github/workflows/build.yml
vendored
43
.github/workflows/build.yml
vendored
@@ -24,31 +24,44 @@ jobs:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
env:
|
||||
NDK_CCACHE: ${{ github.workspace }}/ccache
|
||||
NDK_CCACHE: ccache
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
|
||||
RUSTC_WRAPPER: sccache
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Set up ccache
|
||||
run: bash .github/ccache.sh
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: ${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}
|
||||
|
||||
- name: Set up sccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
variant: sccache
|
||||
key: ${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -58,10 +71,9 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-gradle-
|
||||
|
||||
- name: Cache build cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/.ccache
|
||||
~/.gradle/caches/build-cache-*
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}-build-cache-
|
||||
@@ -71,13 +83,11 @@ jobs:
|
||||
|
||||
- name: Build release
|
||||
run: |
|
||||
./ccache -zp
|
||||
python build.py -vr all
|
||||
|
||||
- name: Build debug
|
||||
run: |
|
||||
python build.py -v all
|
||||
./ccache -s
|
||||
|
||||
- name: Stop gradle daemon
|
||||
run: ./gradlew --stop
|
||||
@@ -85,7 +95,14 @@ jobs:
|
||||
# Only upload artifacts built on Linux
|
||||
- name: Upload build artifact
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
||||
|
||||
- name: Upload mapping and native debug symbols
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ github.sha }}-symbols
|
||||
path: app/build/outputs
|
||||
|
36
.gitmodules
vendored
36
.gitmodules
vendored
@@ -1,42 +1,48 @@
|
||||
[submodule "selinux"]
|
||||
path = native/jni/external/selinux
|
||||
path = native/src/external/selinux
|
||||
url = https://github.com/topjohnwu/selinux.git
|
||||
[submodule "busybox"]
|
||||
path = native/jni/external/busybox
|
||||
path = native/src/external/busybox
|
||||
url = https://github.com/topjohnwu/ndk-busybox.git
|
||||
[submodule "dtc"]
|
||||
path = native/jni/external/dtc
|
||||
path = native/src/external/dtc
|
||||
url = https://github.com/dgibson/dtc.git
|
||||
[submodule "lz4"]
|
||||
path = native/jni/external/lz4
|
||||
path = native/src/external/lz4
|
||||
url = https://github.com/lz4/lz4.git
|
||||
[submodule "bzip2"]
|
||||
path = native/jni/external/bzip2
|
||||
path = native/src/external/bzip2
|
||||
url = https://github.com/nemequ/bzip2.git
|
||||
[submodule "xz"]
|
||||
path = native/jni/external/xz
|
||||
path = native/src/external/xz
|
||||
url = https://github.com/xz-mirror/xz.git
|
||||
[submodule "nanopb"]
|
||||
path = native/jni/external/nanopb
|
||||
path = native/src/external/nanopb
|
||||
url = https://github.com/nanopb/nanopb.git
|
||||
[submodule "mincrypt"]
|
||||
path = native/jni/external/mincrypt
|
||||
path = native/src/external/mincrypt
|
||||
url = https://github.com/topjohnwu/mincrypt.git
|
||||
[submodule "pcre"]
|
||||
path = native/jni/external/pcre
|
||||
path = native/src/external/pcre
|
||||
url = https://android.googlesource.com/platform/external/pcre
|
||||
[submodule "libcxx"]
|
||||
path = native/jni/external/libcxx
|
||||
path = native/src/external/libcxx
|
||||
url = https://github.com/topjohnwu/libcxx.git
|
||||
[submodule "zlib"]
|
||||
path = native/jni/external/zlib
|
||||
path = native/src/external/zlib
|
||||
url = https://android.googlesource.com/platform/external/zlib
|
||||
[submodule "parallel-hashmap"]
|
||||
path = native/jni/external/parallel-hashmap
|
||||
path = native/src/external/parallel-hashmap
|
||||
url = https://github.com/greg7mdp/parallel-hashmap.git
|
||||
[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
|
||||
[submodule "lsplt"]
|
||||
path = native/src/external/lsplt
|
||||
url = https://github.com/LSPosed/LSPlt.git
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
[submodule "zopfli"]
|
||||
path = native/jni/external/zopfli
|
||||
url = https://github.com/google/zopfli.git
|
||||
|
11
README.MD
11
README.MD
@@ -6,7 +6,7 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 5.0.<br>
|
||||
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 6.0.<br>
|
||||
Some highlight features:
|
||||
|
||||
- **MagiskSU**: Provide root access for applications
|
||||
@@ -18,8 +18,8 @@ Some highlight features:
|
||||
|
||||
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
||||
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v24.3)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v24.3)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.2)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.2)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||
|
||||
@@ -27,7 +27,6 @@ Some highlight features:
|
||||
|
||||
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
||||
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
||||
- [Magisk Troubleshoot Wiki](https://www.didgeridoohan.com/magisk/HomePage) (by [@Didgeridoohan](https://github.com/Didgeridoohan))
|
||||
|
||||
## Bug Reports
|
||||
|
||||
@@ -41,7 +40,7 @@ For Magisk app crashes, record and upload the logcat when the crash occurs.
|
||||
|
||||
- Magisk builds on any OS Android Studio supports. Install Android Studio and do the initial setups.
|
||||
- Clone sources: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git`
|
||||
- Install Python 3.6+ \
|
||||
- Install Python 3.8+ \
|
||||
(Windows only: select **'Add Python to PATH'** in installer, and run `pip install colorama` after install)
|
||||
- Configure to use the JDK bundled in Android Studio:
|
||||
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/Contents/Home"`
|
||||
@@ -51,7 +50,7 @@ For Magisk app crashes, record and upload the logcat when the crash occurs.
|
||||
- Run `./build.py ndk` to let the script download and install NDK for you
|
||||
- To start building, run `build.py` to see your options. \
|
||||
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
||||
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native (C++/C) sources.
|
||||
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native sources.
|
||||
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
|
||||
|
||||
## Signing and Distribution
|
||||
|
7
app/.gitignore
vendored
7
app/.gitignore
vendored
@@ -3,10 +3,9 @@
|
||||
/local.properties
|
||||
.idea/
|
||||
/build
|
||||
app/release
|
||||
*.hprof
|
||||
.externalNativeBuild/
|
||||
*.apk
|
||||
src/main/assets
|
||||
src/main/jniLibs
|
||||
src/main/resources
|
||||
src/*/assets
|
||||
src/*/jniLibs
|
||||
src/*/resources
|
||||
|
@@ -26,7 +26,10 @@ android {
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
versionName = Config.version
|
||||
versionCode = Config.versionCode
|
||||
ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
debugSymbolLevel = "FULL"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -39,11 +42,13 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
aidl = true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/*"
|
||||
excludes += "/META-INF/versions/**"
|
||||
excludes += "/org/bouncycastle/**"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "/kotlinx/**"
|
||||
@@ -52,9 +57,6 @@ android {
|
||||
excludes += "/*.bin"
|
||||
excludes += "/*.json"
|
||||
}
|
||||
jniLibs {
|
||||
keepDebugSymbols += "**/*.so"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,13 +74,13 @@ dependencies {
|
||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||
implementation("org.bouncycastle:bcpkix-jdk18on:1.71")
|
||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
|
||||
implementation("dev.rikka.rikkax.insets:insets:1.2.0")
|
||||
implementation("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
|
||||
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
|
||||
implementation("io.noties.markwon:core:4.6.2")
|
||||
|
||||
val vLibsu = "5.0.2"
|
||||
val vLibsu = "5.0.5"
|
||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
||||
@@ -88,33 +90,32 @@ dependencies {
|
||||
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
||||
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
||||
|
||||
val vOkHttp = "4.9.3"
|
||||
val vOkHttp = "4.10.0"
|
||||
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
||||
|
||||
val vMoshi = "1.13.0"
|
||||
val vMoshi = "1.14.0"
|
||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||
|
||||
val vRoom = "2.4.2"
|
||||
val vRoom = "2.5.1"
|
||||
implementation("androidx.room:room-runtime:${vRoom}")
|
||||
implementation("androidx.room:room-ktx:${vRoom}")
|
||||
kapt("androidx.room:room-compiler:${vRoom}")
|
||||
|
||||
val vNav = "2.5.0-rc01"
|
||||
val vNav = "2.5.3"
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||
implementation("androidx.preference:preference:1.2.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.5.6")
|
||||
implementation("androidx.transition:transition:1.4.1")
|
||||
implementation("androidx.core:core-ktx:1.7.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-rc01")
|
||||
implementation("com.google.android.material:material:1.6.0")
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0")
|
||||
implementation("com.google.android.material:material:1.8.0")
|
||||
}
|
||||
|
18
app/proguard-rules.pro
vendored
18
app/proguard-rules.pro
vendored
@@ -11,12 +11,15 @@
|
||||
-assumenosideeffects class java.util.Objects {
|
||||
public static ** requireNonNull(...);
|
||||
}
|
||||
-assumenosideeffects public class kotlin.coroutines.jvm.internal.DebugMetadataKt {
|
||||
private static ** getDebugMetadataAnnotation(...) return null;
|
||||
}
|
||||
|
||||
# Stub
|
||||
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
||||
boolean mActivityHandlesUiModeChecked;
|
||||
boolean mActivityHandlesUiMode;
|
||||
boolean mActivityHandlesConfigFlagsChecked;
|
||||
int mActivityHandlesConfigFlags;
|
||||
}
|
||||
|
||||
# main
|
||||
@@ -30,6 +33,17 @@
|
||||
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'
|
||||
-allowaccessmodification
|
||||
|
@@ -7,7 +7,3 @@ setupCommon()
|
||||
android {
|
||||
namespace = "com.topjohnwu.shared"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("io.michaelrocks:paranoid-core:0.3.7")
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.shared"
|
||||
android:installLocation="internalOnly">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@@ -9,6 +8,10 @@
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29"
|
||||
|
@@ -2,9 +2,6 @@ package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import io.michaelrocks.paranoid.Obfuscate;
|
||||
|
||||
@Obfuscate
|
||||
public class ProviderInstaller {
|
||||
|
||||
public static boolean install(Context context) {
|
||||
|
@@ -3,6 +3,7 @@ package com.topjohnwu.magisk;
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -11,6 +12,7 @@ import android.content.res.AssetManager;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.loader.ResourcesLoader;
|
||||
import android.content.res.loader.ResourcesProvider;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.File;
|
||||
@@ -18,9 +20,6 @@ import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import io.michaelrocks.paranoid.Obfuscate;
|
||||
|
||||
@Obfuscate
|
||||
public class StubApk {
|
||||
private static File dynDir;
|
||||
private static Method addAssetPath;
|
||||
@@ -28,7 +27,7 @@ public class StubApk {
|
||||
private static File getDynDir(ApplicationInfo info) {
|
||||
if (dynDir == null) {
|
||||
final String dataDir;
|
||||
if (SDK_INT >= 24) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// Use device protected path to allow directBootAware
|
||||
dataDir = info.deviceProtectedDataDir;
|
||||
} else {
|
||||
@@ -56,12 +55,24 @@ public class StubApk {
|
||||
return new File(getDynDir(info), "update.apk");
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.R)
|
||||
private static ResourcesLoader getResourcesLoader(File path) throws IOException {
|
||||
var loader = new ResourcesLoader();
|
||||
ResourcesProvider provider;
|
||||
if (path.isDirectory()) {
|
||||
provider = ResourcesProvider.loadFromDirectory(path.getPath(), null);
|
||||
} else {
|
||||
var fd = ParcelFileDescriptor.open(path, MODE_READ_ONLY);
|
||||
provider = ResourcesProvider.loadFromApk(fd);
|
||||
}
|
||||
loader.addProvider(provider);
|
||||
return loader;
|
||||
}
|
||||
|
||||
public static void addAssetPath(Resources res, String path) {
|
||||
if (SDK_INT >= 30) {
|
||||
try (var fd = ParcelFileDescriptor.open(new File(path), MODE_READ_ONLY)) {
|
||||
var loader = new ResourcesLoader();
|
||||
loader.addProvider(ResourcesProvider.loadFromApk(fd));
|
||||
res.addLoaders(loader);
|
||||
if (SDK_INT >= Build.VERSION_CODES.R) {
|
||||
try {
|
||||
res.addLoaders(getResourcesLoader(new File(path)));
|
||||
} catch (IOException ignored) {}
|
||||
} else {
|
||||
AssetManager asset = res.getAssets();
|
||||
|
@@ -25,9 +25,6 @@ import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.michaelrocks.paranoid.Obfuscate;
|
||||
|
||||
@Obfuscate
|
||||
public final class APKInstall {
|
||||
|
||||
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
||||
@@ -39,6 +36,16 @@ public final class APKInstall {
|
||||
}
|
||||
}
|
||||
|
||||
public static void registerReceiver(
|
||||
Context context, BroadcastReceiver receiver, IntentFilter filter) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// noinspection InlinedApi
|
||||
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
} else {
|
||||
context.registerReceiver(receiver, filter);
|
||||
}
|
||||
}
|
||||
|
||||
public static Session startSession(Context context) {
|
||||
return startSession(context, null, null, null);
|
||||
}
|
||||
@@ -51,9 +58,9 @@ public final class APKInstall {
|
||||
// If pkg is not null, look for package added event
|
||||
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
||||
filter.addDataScheme("package");
|
||||
context.registerReceiver(receiver, filter);
|
||||
registerReceiver(context, receiver, filter);
|
||||
}
|
||||
context.registerReceiver(receiver, new IntentFilter(receiver.sessionId));
|
||||
registerReceiver(context, receiver, new IntentFilter(receiver.sessionId));
|
||||
return receiver;
|
||||
}
|
||||
|
||||
@@ -94,27 +101,25 @@ public final class APKInstall {
|
||||
} else if (sessionId.equals(intent.getAction())) {
|
||||
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
||||
switch (status) {
|
||||
case STATUS_PENDING_USER_ACTION:
|
||||
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
break;
|
||||
case STATUS_SUCCESS:
|
||||
case STATUS_PENDING_USER_ACTION ->
|
||||
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
case STATUS_SUCCESS -> {
|
||||
if (packageName == null) {
|
||||
onSuccess(context);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
default -> {
|
||||
int id = intent.getIntExtra(EXTRA_SESSION_ID, 0);
|
||||
if (id > 0) {
|
||||
var installer = context.getPackageManager().getPackageInstaller();
|
||||
var info = installer.getSessionInfo(id);
|
||||
if (info != null) {
|
||||
installer.abandonSession(info.getSessionId());
|
||||
}
|
||||
var installer = context.getPackageManager().getPackageInstaller();
|
||||
try {
|
||||
installer.abandonSession(id);
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
if (onFailure != null) {
|
||||
onFailure.run();
|
||||
}
|
||||
context.getApplicationContext().unregisterReceiver(this);
|
||||
}
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package com.topjohnwu.magisk.utils;
|
||||
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
@@ -14,8 +16,8 @@ public class DynamicClassLoader extends BaseDexClassLoader {
|
||||
}
|
||||
|
||||
public DynamicClassLoader(File apk, ClassLoader parent) {
|
||||
// Set optimizedDirectory to null to bypass DexFile's security checks
|
||||
super(apk.getPath(), null, null, parent);
|
||||
// Set optimizedDirectory to null for RootService to bypass DexFile's security checks
|
||||
super(apk.getPath(), Process.myUid() == 0 ? null : apk.getParentFile(), null, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -2,12 +2,21 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<permission
|
||||
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||
android:protectionLevel="signature"
|
||||
tools:node="remove" />
|
||||
|
||||
<uses-permission
|
||||
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||
tools:node="remove" />
|
||||
|
||||
<application
|
||||
android:name=".core.App"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:multiArch="true"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||
tools:remove="android:appComponentFactory">
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
@@ -27,6 +36,7 @@
|
||||
android:name=".ui.surequest.SuRequestActivity"
|
||||
android:directBootAware="true"
|
||||
android:exported="false"
|
||||
android:taskAffinity=""
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -52,7 +62,8 @@
|
||||
|
||||
<service
|
||||
android:name=".core.download.DownloadService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".core.JobService"
|
||||
|
@@ -0,0 +1,12 @@
|
||||
package androidx.lifecycle;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class ProcessLifecycleAccessor {
|
||||
public static void init(@NonNull Context context) {
|
||||
LifecycleDispatcher.init(context);
|
||||
ProcessLifecycleOwner.init(context);
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class AsyncLoadViewModel : BaseViewModel() {
|
||||
|
||||
private var loadingJob: Job? = null
|
||||
|
||||
@MainThread
|
||||
fun startLoading() {
|
||||
if (loadingJob?.isActive == true) {
|
||||
// Prevent multiple jobs from running at the same time
|
||||
return
|
||||
}
|
||||
loadingJob = viewModelScope.launch { doLoadWork() }
|
||||
}
|
||||
|
||||
protected abstract suspend fun doLoadWork()
|
||||
}
|
@@ -5,6 +5,7 @@ import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.OnRebindCallback
|
||||
import androidx.databinding.ViewDataBinding
|
||||
@@ -66,6 +67,8 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if (this is MenuProvider)
|
||||
activity?.addMenuProvider(this, viewLifecycleOwner)
|
||||
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
|
||||
override fun onPreBind(binding: Binding): Boolean {
|
||||
this@BaseFragment.onPreBind(binding)
|
||||
@@ -76,7 +79,10 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.requestRefresh()
|
||||
viewModel.let {
|
||||
if (it is AsyncLoadViewModel)
|
||||
it.startLoading()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onPreBind(binding: Binding) {
|
||||
|
@@ -1,64 +1,32 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.Manifest.permission.*
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.events.DialogEvent
|
||||
import com.topjohnwu.magisk.events.NavigationEvent
|
||||
import com.topjohnwu.magisk.events.PermissionEvent
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
abstract class BaseViewModel(
|
||||
initialState: State = State.LOADING
|
||||
) : ViewModel(), ObservableHost {
|
||||
abstract class BaseViewModel : ViewModel(), ObservableHost {
|
||||
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
|
||||
enum class State {
|
||||
LOADED, LOADING, LOADING_FAILED
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
val loading get() = state == State.LOADING
|
||||
@get:Bindable
|
||||
val loaded get() = state == State.LOADED
|
||||
@get:Bindable
|
||||
val loadFailed get() = state == State.LOADING_FAILED
|
||||
|
||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||
|
||||
var state= initialState
|
||||
set(value) = set(value, field, { field = it }, BR.loading, BR.loaded, BR.loadFailed)
|
||||
|
||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||
private var runningJob: Job? = null
|
||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||
|
||||
open fun onSaveState(state: Bundle) {}
|
||||
open fun onRestoreState(state: Bundle) {}
|
||||
|
||||
/** This should probably never be called manually, it's called manually via delegate. */
|
||||
@Synchronized
|
||||
fun requestRefresh() {
|
||||
if (runningJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
runningJob = refresh()
|
||||
}
|
||||
|
||||
protected open fun refresh(): Job? = null
|
||||
open fun onNetworkChanged(network: Boolean) {}
|
||||
|
||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||
PermissionEvent(permission, callback).publish()
|
||||
@@ -85,15 +53,25 @@ abstract class BaseViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
inline fun withPostNotificationPermission(crossinline callback: () -> Unit) {
|
||||
withPermission(POST_NOTIFICATIONS) {
|
||||
if (!it) {
|
||||
SnackbarEvent(R.string.post_notifications_denied).publish()
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun back() = BackPressEvent().publish()
|
||||
|
||||
fun <Event : ViewEvent> Event.publish() {
|
||||
fun ViewEvent.publish() {
|
||||
_viewEvents.postValue(this)
|
||||
}
|
||||
|
||||
fun <Event : ViewEventWithScope> Event.publish() {
|
||||
scope = viewModelScope
|
||||
_viewEvents.postValue(this)
|
||||
fun DialogBuilder.show() {
|
||||
DialogEvent(this).publish()
|
||||
}
|
||||
|
||||
fun NavDirections.navigate(pop: Boolean = false) {
|
||||
|
@@ -20,12 +20,14 @@ abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Bindin
|
||||
val navigation: NavController get() = navHostFragment.navController
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
|
||||
return if (binded && currentFragment?.onKeyEvent(event) == true) true else super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (currentFragment?.onBackPressed()?.not() == true) {
|
||||
super.onBackPressed()
|
||||
if (binded) {
|
||||
if (currentFragment?.onBackPressed() == false) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -14,7 +14,6 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.widget.Pre23CardViewBackgroundColorFixLayoutInflaterListener
|
||||
import rikka.insets.WindowInsetsHelper
|
||||
import rikka.layoutinflater.view.LayoutInflaterFactory
|
||||
|
||||
@@ -23,6 +22,8 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
|
||||
protected val binded get() = ::binding.isInitialized
|
||||
|
||||
open val snackbarView get() = binding.root
|
||||
open val snackbarAnchorView: View? get() = null
|
||||
|
||||
@@ -33,11 +34,6 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
||||
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
this.addOnViewCreatedListener(Pre23CardViewBackgroundColorFixLayoutInflaterListener.getInstance())
|
||||
}
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -89,7 +85,10 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.requestRefresh()
|
||||
viewModel.let {
|
||||
if (it is AsyncLoadViewModel)
|
||||
it.startLoading()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Class for passing events from ViewModels to Activities/Fragments
|
||||
@@ -9,10 +8,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
*/
|
||||
abstract class ViewEvent
|
||||
|
||||
abstract class ViewEventWithScope: ViewEvent() {
|
||||
lateinit var scope: CoroutineScope
|
||||
}
|
||||
|
||||
interface ContextExecutor {
|
||||
operator fun invoke(context: Context)
|
||||
}
|
||||
|
@@ -17,12 +17,8 @@ interface ViewModelHolder : LifecycleOwner, ViewModelStoreOwner {
|
||||
val viewModel: BaseViewModel
|
||||
|
||||
fun startObserveLiveData() {
|
||||
viewModel.viewEvents.observe(this) {
|
||||
onEventDispatched(it)
|
||||
}
|
||||
Info.isConnected.observe(this) {
|
||||
viewModel.requestRefresh()
|
||||
}
|
||||
viewModel.viewEvents.observe(this, this::onEventDispatched)
|
||||
Info.isConnected.observe(this, viewModel::onNetworkChanged)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -5,10 +5,16 @@ import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.ProcessLifecycleAccessor
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.*
|
||||
import com.topjohnwu.magisk.core.utils.DispatcherExecutor
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.core.utils.ShellInit
|
||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.core.utils.setConfig
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
@@ -70,6 +76,12 @@ open class App() : Application() {
|
||||
|
||||
refreshLocale()
|
||||
resources.patch()
|
||||
Notifications.setup()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ProcessLifecycleAccessor.init(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
|
@@ -6,6 +6,7 @@ import android.util.Xml
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.repository.BoolDBPropertyNoWrite
|
||||
import com.topjohnwu.magisk.core.repository.DBConfig
|
||||
@@ -136,7 +137,15 @@ object Config : PreferenceConfig, DBConfig {
|
||||
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||
var suTapjack by preference(Key.SU_TAPJACK, true)
|
||||
var checkUpdate by preference(Key.CHECK_UPDATES, true)
|
||||
private var checkUpdatePrefs by preference(Key.CHECK_UPDATES, true)
|
||||
var checkUpdate
|
||||
get() = checkUpdatePrefs
|
||||
set(value) {
|
||||
if (checkUpdatePrefs != value) {
|
||||
checkUpdatePrefs = value
|
||||
JobService.schedule(AppContext)
|
||||
}
|
||||
}
|
||||
var doh by preference(Key.DOH, false)
|
||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||
|
||||
|
@@ -15,8 +15,7 @@ object Const {
|
||||
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
|
||||
|
||||
// Paths
|
||||
lateinit var MAGISKTMP: String
|
||||
val MAGISK_PATH get() = "$MAGISKTMP/modules"
|
||||
const val MAGISK_PATH = "/data/adb/modules"
|
||||
const val TMPDIR = "/dev/tmp"
|
||||
const val MAGISK_LOG = "/cache/magisk.log"
|
||||
|
||||
|
@@ -7,10 +7,9 @@ import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
|
||||
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
||||
import com.topjohnwu.magisk.ktx.getProperty
|
||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
|
||||
val isRunningAsStub get() = Info.stub != null
|
||||
|
||||
@@ -47,7 +46,8 @@ object Info {
|
||||
val isConnected: LiveData<Boolean> by lazy {
|
||||
MutableLiveData(false).also { field ->
|
||||
NetworkObserver.observe(AppContext) {
|
||||
UiThreadHandler.run { field.value = it }
|
||||
remote = EMPTY_REMOTE
|
||||
field.postValue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,9 +67,10 @@ object Info {
|
||||
) {
|
||||
val versionCode = when {
|
||||
code < Const.Version.MIN_VERCODE -> -1
|
||||
else -> if (isRooted) code else -1
|
||||
isRooted -> code
|
||||
else -> -1
|
||||
}
|
||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||
val isActive = versionCode >= 0
|
||||
val isActive = versionCode > 0
|
||||
}
|
||||
}
|
||||
|
@@ -33,7 +33,7 @@ class JobService : BaseJobService() {
|
||||
svc.fetchUpdate()?.let {
|
||||
Info.remote = it
|
||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
||||
Notifications.updateAvailable(this)
|
||||
Notifications.updateAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -51,7 +51,7 @@ open class Receiver : BaseReceiver() {
|
||||
@Suppress("DEPRECATION")
|
||||
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
||||
if (installer == context.packageName) {
|
||||
Notifications.updateDone(context)
|
||||
Notifications.updateDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.app.Activity
|
||||
@@ -35,9 +36,11 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
permissionCallback?.invoke(it)
|
||||
permissionCallback = null
|
||||
}
|
||||
|
||||
private var installCallback: ((Boolean) -> Unit)? = null
|
||||
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
||||
permissionCallback?.invoke(it)
|
||||
permissionCallback = null
|
||||
installCallback?.invoke(it)
|
||||
installCallback = null
|
||||
}
|
||||
|
||||
private var contentCallback: ContentResultCallback? = null
|
||||
@@ -52,9 +55,7 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
val realCallingPackage: String? get() {
|
||||
callingPackage?.let { return it }
|
||||
if (Build.VERSION.SDK_INT >= 22) {
|
||||
mReferrerField.get(this)?.let { return it as String }
|
||||
}
|
||||
mReferrerField.get(this)?.let { return it as String }
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -67,8 +68,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
// Overwrite private members to avoid nasty "false" stack traces being logged
|
||||
val delegate = delegate
|
||||
val clz = delegate.javaClass
|
||||
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
|
||||
clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
|
||||
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
|
||||
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
|
||||
}
|
||||
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -82,15 +83,23 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
||||
// We do not need external rw on 30+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||
permission == WRITE_EXTERNAL_STORAGE) {
|
||||
// We do not need external rw on R+
|
||||
callback(true)
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
|
||||
permission == POST_NOTIFICATIONS) {
|
||||
// All apps have notification permissions before T
|
||||
callback(true)
|
||||
return
|
||||
}
|
||||
permissionCallback = callback
|
||||
if (permission == REQUEST_INSTALL_PACKAGES) {
|
||||
installCallback = callback
|
||||
requestInstall.launch(Unit)
|
||||
} else {
|
||||
permissionCallback = callback
|
||||
requestPermission.launch(permission)
|
||||
}
|
||||
}
|
||||
|
@@ -12,15 +12,12 @@ private const val FILE = "file"
|
||||
|
||||
interface GithubPageServices {
|
||||
|
||||
@GET("{$FILE}")
|
||||
suspend fun fetchUpdateJSON(@Path(FILE) file: String): UpdateInfo
|
||||
@GET
|
||||
suspend fun fetchUpdateJSON(@Url file: String): UpdateInfo
|
||||
}
|
||||
|
||||
interface RawServices {
|
||||
|
||||
@GET
|
||||
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
|
||||
|
||||
@GET
|
||||
@Streaming
|
||||
suspend fun fetchFile(@Url url: String): ResponseBody
|
||||
|
@@ -39,7 +39,6 @@ object ServiceLocator {
|
||||
NetworkService(
|
||||
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
||||
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
||||
createApiService(retrofit, Const.Url.GITHUB_API_URL)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.*
|
||||
@@ -13,26 +14,26 @@ import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.ActivityTracker
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.ktx.forEach
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.ktx.*
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.Properties
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
@@ -46,14 +47,12 @@ class DownloadService : NotificationService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun download(subject: Subject) {
|
||||
update(subject.notifyId)
|
||||
val coroutineScope = CoroutineScope(job + Dispatchers.IO)
|
||||
coroutineScope.launch {
|
||||
notifyUpdate(subject.notifyId)
|
||||
CoroutineScope(job + Dispatchers.IO).launch {
|
||||
try {
|
||||
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
||||
when (subject) {
|
||||
@@ -62,7 +61,7 @@ class DownloadService : NotificationService() {
|
||||
}
|
||||
val activity = ActivityTracker.foreground
|
||||
if (activity != null && subject.autoLaunch) {
|
||||
remove(subject.notifyId)
|
||||
notifyRemove(subject.notifyId)
|
||||
subject.pendingIntent(activity)?.send()
|
||||
} else {
|
||||
notifyFinish(subject)
|
||||
@@ -77,9 +76,9 @@ class DownloadService : NotificationService() {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||
private fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||
fun writeTee(output: OutputStream) {
|
||||
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||
val external = uri.outputStream()
|
||||
stream.copyAndClose(TeeOutputStream(external, output))
|
||||
}
|
||||
@@ -90,35 +89,34 @@ class DownloadService : NotificationService() {
|
||||
// Download full APK to stub update path
|
||||
writeTee(updateApk.outputStream())
|
||||
|
||||
if (Info.stub!!.version < subject.stub.versionCode) {
|
||||
val zf = ZipFile(updateApk)
|
||||
val prop = Properties()
|
||||
prop.load(ByteArrayInputStream(zf.comment.toByteArray()))
|
||||
val stubVersion = prop.getProperty("stubVersion").toIntOrNull() ?: -1
|
||||
if (Info.stub!!.version < stubVersion) {
|
||||
// Also upgrade stub
|
||||
update(subject.notifyId) {
|
||||
notifyUpdate(subject.notifyId) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_app_title))
|
||||
.setContentText("")
|
||||
}
|
||||
|
||||
// Download
|
||||
// Extract stub
|
||||
val apk = subject.file.toFile()
|
||||
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
||||
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||
zf.close()
|
||||
|
||||
// Patch and install
|
||||
val session = APKInstall.startSession(this)
|
||||
session.openStream(this).use {
|
||||
val label = applicationInfo.nonLocalizedLabel
|
||||
if (!HideAPK.patch(this, apk, it, packageName, label)) {
|
||||
throw IOException("HideAPK patch error")
|
||||
}
|
||||
}
|
||||
subject.intent = HideAPK.upgrade(this, apk)
|
||||
?: throw IOException("HideAPK patch error")
|
||||
apk.delete()
|
||||
subject.intent = session.waitIntent()
|
||||
} else {
|
||||
ActivityTracker.foreground?.let {
|
||||
// Relaunch the process if we are foreground
|
||||
StubApk.restartProcess(it)
|
||||
} ?: run {
|
||||
// Or else kill the current process after posting notification
|
||||
subject.intent = Notifications.selfLaunchIntent(this)
|
||||
subject.intent = selfLaunchIntent()
|
||||
subject.postDownload = { Runtime.getRuntime().exit(0) }
|
||||
}
|
||||
return
|
||||
@@ -199,19 +197,23 @@ class DownloadService : NotificationService() {
|
||||
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
|
||||
val intent = intent(context, subject)
|
||||
return if (Build.VERSION.SDK_INT >= 26) {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||
} else {
|
||||
getService(context, REQUEST_CODE, intent, flag)
|
||||
}
|
||||
}
|
||||
|
||||
fun start(context: Context, subject: Subject) {
|
||||
val app = context.applicationContext
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
app.startForegroundService(intent(app, subject))
|
||||
} else {
|
||||
app.startService(intent(app, subject))
|
||||
@SuppressLint("InlinedApi")
|
||||
fun start(activity: BaseActivity, subject: Subject) {
|
||||
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||
// Always download regardless of notification permission status
|
||||
val app = activity.applicationContext
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
app.startForegroundService(intent(app, subject))
|
||||
} else {
|
||||
app.startService(intent(app, subject))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
@@ -19,6 +20,8 @@ open class NotificationService : BaseService() {
|
||||
|
||||
protected val service get() = ServiceLocator.networkService
|
||||
|
||||
private var attachedNotificationId = 0
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
notifications.forEach { Notifications.mgr.cancel(it.key) }
|
||||
@@ -30,11 +33,11 @@ open class NotificationService : BaseService() {
|
||||
val total = max.toFloat() / 1048576
|
||||
val id = subject.notifyId
|
||||
|
||||
update(id) { it.setContentTitle(subject.title) }
|
||||
notifyUpdate(id) { it.setContentTitle(subject.title) }
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it.toFloat() / 1048576
|
||||
update(id) { notification ->
|
||||
notifyUpdate(id) { notification ->
|
||||
if (max > 0) {
|
||||
broadcast(progress / total, subject)
|
||||
notification
|
||||
@@ -49,7 +52,7 @@ open class NotificationService : BaseService() {
|
||||
}
|
||||
|
||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||
val notification = remove(id)?.also(editor) ?: return -1
|
||||
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
||||
val newId = Notifications.nextId()
|
||||
Notifications.mgr.notify(newId, notification.build())
|
||||
return newId
|
||||
@@ -73,29 +76,44 @@ open class NotificationService : BaseService() {
|
||||
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
|
||||
}
|
||||
|
||||
private fun create() = Notifications.progress(this, "")
|
||||
private fun attachNotification(id: Int, notification: Notification) {
|
||||
attachedNotificationId = id
|
||||
startForeground(id, notification)
|
||||
}
|
||||
|
||||
private fun updateForeground() {
|
||||
private fun maybeDetachNotification(id: Int) : Boolean {
|
||||
if (attachedNotificationId != id) return false
|
||||
if (hasNotifications) {
|
||||
val (id, notification) = notifications.entries.first()
|
||||
startForeground(id, notification.build())
|
||||
} else {
|
||||
stopForeground(false)
|
||||
val (anotherId, notification) = notifications.entries.first()
|
||||
// Attaching a new notification will remove the current showing one
|
||||
attachNotification(anotherId, notification.build())
|
||||
return true
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
}
|
||||
attachedNotificationId = 0
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
protected fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
fun create() = Notifications.startProgress("")
|
||||
|
||||
val wasEmpty = !hasNotifications
|
||||
val notification = notifications.getOrPut(id, ::create).also(editor)
|
||||
val notification = notifications.getOrPut(id, ::create).also(editor).build()
|
||||
if (wasEmpty)
|
||||
updateForeground()
|
||||
attachNotification(id, notification)
|
||||
else
|
||||
Notifications.mgr.notify(id, notification.build())
|
||||
Notifications.mgr.notify(id, notification)
|
||||
}
|
||||
|
||||
protected fun remove(id: Int): Notification.Builder? {
|
||||
val n = notifications.remove(id)?.also { updateForeground() }
|
||||
Notifications.mgr.cancel(id)
|
||||
protected fun notifyRemove(id: Int): Notification.Builder? {
|
||||
val n = notifications.remove(id)
|
||||
if (n == null || !maybeDetachNotification(id))
|
||||
Notifications.mgr.cancel(id)
|
||||
return n
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,6 @@ import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||
import com.topjohnwu.magisk.core.model.StubJson
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.ktx.cachedFile
|
||||
@@ -59,7 +58,6 @@ sealed class Subject : Parcelable {
|
||||
@Parcelize
|
||||
class App(
|
||||
private val json: MagiskJson = Info.remote.magisk,
|
||||
val stub: StubJson = Info.remote.stub,
|
||||
override val notifyId: Int = Notifications.nextId()
|
||||
) : Subject() {
|
||||
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||
|
@@ -7,7 +7,6 @@ import kotlinx.parcelize.Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateInfo(
|
||||
val magisk: MagiskJson = MagiskJson(),
|
||||
val stub: StubJson = StubJson()
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@@ -19,13 +18,6 @@ data class MagiskJson(
|
||||
val note: String = ""
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StubJson(
|
||||
val versionCode: Int = -1,
|
||||
val link: String = ""
|
||||
) : Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ModuleJson(
|
||||
val version: String,
|
||||
|
@@ -43,10 +43,10 @@ data class LocalModule(
|
||||
set(enable) {
|
||||
if (enable) {
|
||||
disableFile.delete()
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
} else {
|
||||
!disableFile.createNewFile()
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +56,10 @@ data class LocalModule(
|
||||
if (remove) {
|
||||
if (updateFile.exists()) return
|
||||
removeFile.createNewFile()
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
} else {
|
||||
removeFile.delete()
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,15 +122,13 @@ data class LocalModule(
|
||||
|
||||
companion object {
|
||||
|
||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
||||
|
||||
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
||||
|
||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
||||
.listFiles()
|
||||
.orEmpty()
|
||||
.filter { !it.isFile }
|
||||
.filter { !it.isFile && !it.isHidden }
|
||||
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
||||
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
||||
}
|
||||
|
@@ -8,7 +8,6 @@ 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.GithubApiServices
|
||||
import com.topjohnwu.magisk.core.data.GithubPageServices
|
||||
import com.topjohnwu.magisk.core.data.RawServices
|
||||
import retrofit2.HttpException
|
||||
@@ -17,8 +16,7 @@ import java.io.IOException
|
||||
|
||||
class NetworkService(
|
||||
private val pages: GithubPageServices,
|
||||
private val raw: RawServices,
|
||||
private val api: GithubApiServices
|
||||
private val raw: RawServices
|
||||
) {
|
||||
suspend fun fetchUpdate() = safe {
|
||||
var info = when (Config.updateChannel) {
|
||||
@@ -42,11 +40,14 @@ class NetworkService(
|
||||
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) = raw.fetchCustomUpdate(url)
|
||||
private suspend fun fetchCustomUpdate(url: String) = pages.fetchUpdateJSON(url)
|
||||
|
||||
private inline fun <T> safe(factory: () -> T): T? {
|
||||
return try {
|
||||
factory()
|
||||
if (Info.isConnected.value == true)
|
||||
factory()
|
||||
else
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
null
|
||||
|
@@ -4,14 +4,13 @@ import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.Provider
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.AXML
|
||||
import com.topjohnwu.magisk.core.utils.Keygen
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
@@ -30,6 +29,7 @@ import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.SecureRandom
|
||||
import kotlin.random.asKotlinRandom
|
||||
|
||||
object HideAPK {
|
||||
|
||||
@@ -39,8 +39,7 @@ object HideAPK {
|
||||
|
||||
// Some arbitrary limit
|
||||
const val MAX_LABEL_LENGTH = 32
|
||||
|
||||
private val svc get() = ServiceLocator.networkService
|
||||
const val PLACEHOLDER = "COMPONENT_PLACEHOLDER"
|
||||
|
||||
private fun genPackageName(): String {
|
||||
val random = SecureRandom()
|
||||
@@ -65,20 +64,87 @@ object HideAPK {
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun patch(
|
||||
private fun classNameGenerator() = sequence {
|
||||
val c1 = mutableListOf<String>()
|
||||
val c2 = mutableListOf<String>()
|
||||
val c3 = mutableListOf<String>()
|
||||
val random = SecureRandom()
|
||||
val kRandom = random.asKotlinRandom()
|
||||
|
||||
fun <T> chain(vararg iters: Iterable<T>) = sequence {
|
||||
iters.forEach { it.forEach { v -> yield(v) } }
|
||||
}
|
||||
|
||||
for (a in chain('a'..'z', 'A'..'Z')) {
|
||||
if (a != 'a' && a != 'A') {
|
||||
c1.add("$a")
|
||||
}
|
||||
for (b in chain('a'..'z', 'A'..'Z', '0'..'9')) {
|
||||
c2.add("$a$b")
|
||||
for (c in chain('a'..'z', 'A'..'Z', '0'..'9')) {
|
||||
c3.add("$a$b$c")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c1.shuffle(random)
|
||||
c2.shuffle(random)
|
||||
c3.shuffle(random)
|
||||
|
||||
fun notJavaKeyword(name: String) = when (name) {
|
||||
"do", "if", "for", "int", "new", "try" -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
fun List<String>.process() = asSequence().filter(::notJavaKeyword)
|
||||
|
||||
val names = mutableListOf<String>()
|
||||
names.addAll(c1)
|
||||
names.addAll(c2.process().take(30))
|
||||
names.addAll(c3.process().take(30))
|
||||
|
||||
while (true) {
|
||||
val seg = 2 + random.nextInt(4)
|
||||
val cls = StringBuilder()
|
||||
for (i in 0 until seg) {
|
||||
cls.append(names.random(kRandom))
|
||||
if (i != seg - 1)
|
||||
cls.append('.')
|
||||
}
|
||||
// Old Android does not support capitalized package names
|
||||
// Check Android 7.0.0 PackageParser#buildClassName
|
||||
cls[0] = cls[0].lowercaseChar()
|
||||
yield(cls.toString())
|
||||
}
|
||||
}.distinct().iterator()
|
||||
|
||||
private fun patch(
|
||||
context: Context,
|
||||
apk: File, out: OutputStream,
|
||||
pkg: String, label: CharSequence
|
||||
): Boolean {
|
||||
val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
|
||||
val name = info.applicationInfo.nonLocalizedLabel.toString()
|
||||
val origLabel = info.applicationInfo.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.findAndPatch(APPLICATION_ID to pkg, name to label.toString()))
|
||||
if (!xml.patchStrings {
|
||||
for (i in it.indices) {
|
||||
val s = it[i]
|
||||
if (s.contains(APPLICATION_ID)) {
|
||||
it[i] = s.replace(APPLICATION_ID, pkg)
|
||||
} else if (s.contains(PLACEHOLDER)) {
|
||||
it[i] = generator.next()
|
||||
} else if (s == origLabel) {
|
||||
it[i] = label.toString()
|
||||
}
|
||||
}
|
||||
}) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Write apk changes
|
||||
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||
@@ -94,7 +160,6 @@ object HideAPK {
|
||||
|
||||
private fun launchApp(activity: Activity, pkg: String) {
|
||||
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
|
||||
val self = activity.packageName
|
||||
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
||||
@@ -103,17 +168,13 @@ object HideAPK {
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
||||
private fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
||||
val stub = File(activity.cacheDir, "stub.apk")
|
||||
try {
|
||||
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
|
||||
activity.assets.open("stub.apk").writeTo(stub)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
stub.createNewFile()
|
||||
val cmd = "\$MAGISKBIN/magiskinit -x manager ${stub.path}"
|
||||
if (!Shell.cmd(cmd).exec().isSuccess)
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
// Generate a new random package name and signature
|
||||
@@ -129,7 +190,8 @@ object HideAPK {
|
||||
launchApp(activity, pkg)
|
||||
}
|
||||
|
||||
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
|
||||
Config.suManager = pkg
|
||||
val cmd = "adb_pm_install $repack $pkg"
|
||||
if (Shell.cmd(cmd).exec().isSuccess) return true
|
||||
|
||||
try {
|
||||
@@ -177,7 +239,8 @@ object HideAPK {
|
||||
launchApp(activity, APPLICATION_ID)
|
||||
dialog.dismiss()
|
||||
}
|
||||
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
|
||||
Config.suManager = ""
|
||||
val cmd = "adb_pm_install $apk $APPLICATION_ID"
|
||||
if (Shell.cmd(cmd).await().isSuccess) return
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
@@ -191,4 +254,17 @@ object HideAPK {
|
||||
}
|
||||
if (!success) onFailure.run()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun upgrade(context: Context, apk: File): Intent? {
|
||||
val label = context.applicationInfo.nonLocalizedLabel
|
||||
val pkg = context.packageName
|
||||
val session = APKInstall.startSession(context)
|
||||
session.openStream(context).use {
|
||||
if (!patch(context, apk, it, pkg, label)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return session.waitIntent()
|
||||
}
|
||||
}
|
||||
|
@@ -37,6 +37,7 @@ import java.io.*
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
@@ -57,7 +58,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
private val localFS get() = FileSystemManager.getLocal()
|
||||
|
||||
private fun findImage(): Boolean {
|
||||
val bootPath = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||
val bootPath = "RECOVERYMODE=${Config.recovery} find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||
if (bootPath.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
@@ -130,7 +131,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
}
|
||||
|
||||
// Extract scripts
|
||||
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh")) {
|
||||
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh", "stub.apk")) {
|
||||
val dest = File(installDir, script)
|
||||
context.assets.open(script).writeTo(dest)
|
||||
}
|
||||
@@ -189,6 +190,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
|
||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||
if (entry.name.startsWith("boot.img") ||
|
||||
entry.name.startsWith("init_boot.img") ||
|
||||
(Config.recovery && entry.name.contains("recovery.img"))) {
|
||||
val name = entry.name.replace(".lz4", "")
|
||||
console.add("-- Extracting: $name")
|
||||
@@ -215,6 +217,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
}
|
||||
}
|
||||
val boot = installDir.getChildFile("boot.img")
|
||||
val initBoot = installDir.getChildFile("init_boot.img")
|
||||
val recovery = installDir.getChildFile("recovery.img")
|
||||
if (Config.recovery && recovery.exists() && boot.exists()) {
|
||||
// Install to recovery
|
||||
@@ -235,11 +238,14 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
}
|
||||
boot.delete()
|
||||
} else {
|
||||
if (!boot.exists()) {
|
||||
console.add("! No boot image found")
|
||||
throw IOException()
|
||||
srcBoot = when {
|
||||
initBoot.exists() -> initBoot
|
||||
boot.exists() -> boot
|
||||
else -> {
|
||||
console.add("! No boot image found")
|
||||
throw IOException()
|
||||
}
|
||||
}
|
||||
srcBoot = boot
|
||||
}
|
||||
return tarOut
|
||||
}
|
||||
@@ -299,7 +305,13 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
try {
|
||||
val newBoot = installDir.getChildFile("new-boot.img")
|
||||
if (outStream is TarOutputStream) {
|
||||
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
|
||||
val name = with(srcBoot.path) {
|
||||
when {
|
||||
contains("recovery") -> "recovery.img"
|
||||
contains("init_boot") -> "init_boot.img"
|
||||
else -> "boot.img"
|
||||
}
|
||||
}
|
||||
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||
}
|
||||
newBoot.newInputStream().cleanPump(outStream)
|
||||
@@ -354,6 +366,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
"KEEPVERITY=${Config.keepVerity} " +
|
||||
"PATCHVBMETAFLAG=${Config.patchVbmeta} " +
|
||||
"RECOVERYMODE=${Config.recovery} " +
|
||||
"SYSTEM_ROOT=${Info.isSAR} " +
|
||||
"sh boot_patch.sh $srcBoot")
|
||||
|
||||
if (!cmds.sh().isSuccess)
|
||||
@@ -420,20 +433,15 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
protected abstract suspend fun operations(): Boolean
|
||||
|
||||
open suspend fun exec(): Boolean {
|
||||
synchronized(Companion) {
|
||||
if (haveActiveSession)
|
||||
return false
|
||||
haveActiveSession = true
|
||||
}
|
||||
if (haveActiveSession.getAndSet(true))
|
||||
return false
|
||||
val result = withContext(Dispatchers.IO) { operations() }
|
||||
synchronized(Companion) {
|
||||
haveActiveSession = false
|
||||
}
|
||||
haveActiveSession.set(false)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var haveActiveSession = false
|
||||
private var haveActiveSession = AtomicBoolean(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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 findAndPatch(vararg patterns: Pair<String, String>): Boolean {
|
||||
fun patchStrings(patchFn: (Array<String>) -> Unit): Boolean {
|
||||
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
||||
|
||||
fun findStringPool(): Int {
|
||||
@@ -42,7 +42,6 @@ class AXML(b: ByteArray) {
|
||||
return -1
|
||||
}
|
||||
|
||||
var patch = false
|
||||
val start = findStringPool()
|
||||
if (start < 0)
|
||||
return false
|
||||
@@ -57,34 +56,26 @@ class AXML(b: ByteArray) {
|
||||
val dataOff = start + intBuf.get()
|
||||
intBuf.get()
|
||||
|
||||
val strings = ArrayList<String>(count)
|
||||
// Read and patch all strings
|
||||
loop@ for (i in 0 until count) {
|
||||
val strList = ArrayList<String>(count)
|
||||
// Collect all strings in the pool
|
||||
for (i in 0 until count) {
|
||||
val off = dataOff + intBuf.get()
|
||||
val len = buffer.getShort(off)
|
||||
val str = String(bytes, off + 2, len * 2, UTF_16LE)
|
||||
for ((from, to) in patterns) {
|
||||
if (str.contains(from)) {
|
||||
strings.add(str.replace(from, to))
|
||||
patch = true
|
||||
continue@loop
|
||||
}
|
||||
}
|
||||
strings.add(str)
|
||||
strList.add(String(bytes, off + 2, len * 2, UTF_16LE))
|
||||
}
|
||||
|
||||
if (!patch)
|
||||
return false
|
||||
val strArr = strList.toTypedArray()
|
||||
patchFn(strArr)
|
||||
|
||||
// Write everything before string data, will patch values later
|
||||
val baos = RawByteStream()
|
||||
baos.write(bytes, 0, dataOff)
|
||||
|
||||
// Write string data
|
||||
val strList = IntArray(count)
|
||||
val offList = IntArray(count)
|
||||
for (i in 0 until count) {
|
||||
strList[i] = baos.size() - dataOff
|
||||
val str = strings[i]
|
||||
offList[i] = baos.size() - dataOff
|
||||
val str = strArr[i]
|
||||
baos.write(str.length.toShortBytes())
|
||||
baos.write(str.toByteArray(UTF_16LE))
|
||||
// Null terminate
|
||||
@@ -103,7 +94,7 @@ class AXML(b: ByteArray) {
|
||||
// Patch index table
|
||||
newBuffer.position(start + STRING_INDICES_OFF)
|
||||
val newIntBuf = newBuffer.asIntBuffer()
|
||||
strList.forEach { newIntBuf.put(it) }
|
||||
offList.forEach { newIntBuf.put(it) }
|
||||
|
||||
// Write the rest of the chunks
|
||||
val nextOff = start + size
|
||||
|
@@ -87,7 +87,7 @@ object MediaStoreUtils {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
|
||||
if (Build.VERSION.SDK_INT < 30) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
// Fallback to file based I/O pre Android 11
|
||||
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
||||
parent.mkdirs()
|
||||
|
@@ -0,0 +1,79 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
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.ktx.registerRuntimeReceiver
|
||||
|
||||
typealias ConnectionCallback = (Boolean) -> Unit
|
||||
|
||||
class NetworkObserver(
|
||||
context: Context,
|
||||
private val callback: ConnectionCallback
|
||||
): DefaultLifecycleObserver {
|
||||
private val manager = context.getSystemService<ConnectivityManager>()!!
|
||||
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
private val activeList = ArraySet<Network>()
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
activeList.add(network)
|
||||
callback(true)
|
||||
}
|
||||
override fun onLost(network: Network) {
|
||||
activeList.remove(network)
|
||||
callback(!activeList.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
private fun Context.isIdleMode(): Boolean {
|
||||
val pwm = getSystemService<PowerManager>() ?: return true
|
||||
val isIgnoringOptimizations = pwm.isIgnoringBatteryOptimizations(packageName)
|
||||
return pwm.isDeviceIdleMode && !isIgnoringOptimizations
|
||||
}
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (context.isIdleMode()) {
|
||||
callback(false)
|
||||
} else {
|
||||
postCurrentState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
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() {
|
||||
callback(manager.getNetworkCapabilities(manager.activeNetwork)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun observe(context: Context, callback: ConnectionCallback): NetworkObserver {
|
||||
return NetworkObserver(context, callback).apply { postCurrentState() }
|
||||
}
|
||||
}
|
||||
}
|
@@ -25,7 +25,7 @@ class RequestInstall : ActivityResultContract<Unit, Boolean>() {
|
||||
context: Context,
|
||||
input: Unit
|
||||
): SynchronousResult<Boolean>? {
|
||||
if (Build.VERSION.SDK_INT < 26)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
return SynchronousResult(true)
|
||||
if (context.packageManager.canRequestPackageInstalls())
|
||||
return SynchronousResult(true)
|
||||
|
@@ -111,15 +111,23 @@ class RootUtils(stub: Any?) : RootService() {
|
||||
}
|
||||
|
||||
fun await() {
|
||||
// We cannot await on the main thread
|
||||
if (Info.isRooted && !ShellUtils.onMainThread())
|
||||
if (!Info.isRooted)
|
||||
return
|
||||
if (!ShellUtils.onMainThread()) {
|
||||
acquireSharedInterruptibly(1)
|
||||
} else if (state != 0) {
|
||||
throw IllegalStateException("Cannot await on the main thread")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var bindTask: Shell.Task? = null
|
||||
var fs = FileSystemManager.getLocal()
|
||||
var fs: FileSystemManager = FileSystemManager.getLocal()
|
||||
get() {
|
||||
Connection.await()
|
||||
return field
|
||||
}
|
||||
private set
|
||||
var obj: IRootUtils? = null
|
||||
get() {
|
||||
|
@@ -41,7 +41,7 @@ class ShellInit : Shell.Initializer() {
|
||||
}
|
||||
|
||||
if (shell.isRoot) {
|
||||
add("export MAGISKTMP=\$(magisk --path)/.magisk")
|
||||
add("export MAGISKTMP=\$(magisk --path)")
|
||||
// Test if we can properly execute stuff in /data
|
||||
Info.noDataExec = !shell.newJob().add("$localBB sh -c \"$localBB true\"").exec().isSuccess
|
||||
}
|
||||
@@ -49,12 +49,12 @@ class ShellInit : Shell.Initializer() {
|
||||
if (Info.noDataExec) {
|
||||
// Copy it out of /data to workaround Samsung bullshit
|
||||
add(
|
||||
"if [ -x \$MAGISKTMP/busybox/busybox ]; then",
|
||||
" cp -af $localBB \$MAGISKTMP/busybox/busybox",
|
||||
" exec \$MAGISKTMP/busybox/busybox sh",
|
||||
"if [ -x \$MAGISKTMP/.magisk/busybox/busybox ]; then",
|
||||
" cp -af $localBB \$MAGISKTMP/.magisk/busybox/busybox",
|
||||
" exec \$MAGISKTMP/.magisk/busybox/busybox sh",
|
||||
"else",
|
||||
" cp -af $localBB /dev/.busybox",
|
||||
" exec /dev/.busybox sh",
|
||||
" cp -af $localBB /dev/busybox",
|
||||
" exec /dev/busybox sh",
|
||||
"fi"
|
||||
)
|
||||
} else {
|
||||
@@ -73,7 +73,6 @@ class ShellInit : Shell.Initializer() {
|
||||
fun getVar(name: String) = fastCmd("echo \$$name")
|
||||
fun getBool(name: String) = getVar(name).toBoolean()
|
||||
|
||||
Const.MAGISKTMP = getVar("MAGISKTMP")
|
||||
Info.isSAR = getBool("SYSTEM_ROOT")
|
||||
Info.ramdisk = getBool("RAMDISKEXIST")
|
||||
Info.vbmeta = getBool("VBMETAEXIST")
|
||||
|
@@ -1,49 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.utils.net
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import androidx.collection.ArraySet
|
||||
|
||||
@TargetApi(21)
|
||||
open class LollipopNetworkObserver(
|
||||
context: Context,
|
||||
callback: ConnectionCallback
|
||||
): NetworkObserver(context, callback) {
|
||||
|
||||
private val networkCallback = NetCallback()
|
||||
|
||||
init {
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
manager.registerNetworkCallback(request, networkCallback)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun getCurrentState() {
|
||||
callback(manager.activeNetworkInfo?.isConnected ?: false)
|
||||
}
|
||||
|
||||
override fun stopObserving() {
|
||||
manager.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
private inner class NetCallback : ConnectivityManager.NetworkCallback() {
|
||||
|
||||
private val activeList = ArraySet<Network>()
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
activeList.add(network)
|
||||
callback(true)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
activeList.remove(network)
|
||||
callback(!activeList.isEmpty())
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.topjohnwu.magisk.core.utils.net
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
@TargetApi(23)
|
||||
class MarshmallowNetworkObserver(
|
||||
context: Context,
|
||||
callback: ConnectionCallback
|
||||
): LollipopNetworkObserver(context, callback) {
|
||||
|
||||
private val receiver = IdleBroadcastReceiver()
|
||||
|
||||
init {
|
||||
val filter = IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)
|
||||
app.registerReceiver(receiver, filter)
|
||||
}
|
||||
|
||||
override fun stopObserving() {
|
||||
super.stopObserving()
|
||||
app.unregisterReceiver(receiver)
|
||||
}
|
||||
|
||||
override fun getCurrentState() {
|
||||
callback(manager.getNetworkCapabilities(manager.activeNetwork)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false)
|
||||
}
|
||||
|
||||
private inner class IdleBroadcastReceiver: BroadcastReceiver() {
|
||||
|
||||
private fun Context.isIdleMode(): Boolean {
|
||||
val pwm = getSystemService<PowerManager>() ?: return true
|
||||
val isIgnoringOptimizations = pwm.isIgnoringBatteryOptimizations(packageName)
|
||||
return pwm.isDeviceIdleMode && !isIgnoringOptimizations
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (context.isIdleMode()) {
|
||||
callback(false)
|
||||
} else {
|
||||
getCurrentState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.utils.net
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
typealias ConnectionCallback = (Boolean) -> Unit
|
||||
|
||||
abstract class NetworkObserver(
|
||||
context: Context,
|
||||
protected val callback: ConnectionCallback
|
||||
) {
|
||||
|
||||
protected val app: Context = context.applicationContext
|
||||
protected val manager = context.getSystemService<ConnectivityManager>()!!
|
||||
|
||||
protected abstract fun stopObserving()
|
||||
protected abstract fun getCurrentState()
|
||||
|
||||
companion object {
|
||||
fun observe(context: Context, callback: ConnectionCallback): NetworkObserver {
|
||||
val observer: NetworkObserver = if (Build.VERSION.SDK_INT >= 23)
|
||||
MarshmallowNetworkObserver(context, callback)
|
||||
else LollipopNetworkObserver(context, callback)
|
||||
return observer.apply { getCurrentState() }
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,37 +1,28 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.databinding.ListChangeRegistry
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.AbstractList
|
||||
|
||||
/**
|
||||
* @param callback The callback that controls the behavior of the DiffObservableList.
|
||||
* @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
|
||||
*/
|
||||
open class DiffObservableList<T>(
|
||||
private val callback: Callback<T>,
|
||||
private val detectMoves: Boolean = true
|
||||
) : AbstractList<T>(), ObservableList<T> {
|
||||
open class DiffObservableList<T : DiffItem<*>>
|
||||
: AbstractList<T>(), ObservableList<T>, ListUpdateCallback {
|
||||
|
||||
protected var list: MutableList<T> = ArrayList()
|
||||
protected var list: List<T> = emptyList()
|
||||
private set
|
||||
private val listeners = ListChangeRegistry()
|
||||
protected val listCallback = ObservableListUpdateCallback()
|
||||
|
||||
override val size: Int get() = list.size
|
||||
|
||||
/**
|
||||
* Calculates the list of update operations that can convert this list into the given one.
|
||||
*
|
||||
* @param newItems The items that this list will be set to.
|
||||
* @return A DiffResult that contains the information about the edit sequence to covert this
|
||||
* list into the given one.
|
||||
*/
|
||||
override fun get(index: Int) = list[index]
|
||||
|
||||
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||
val frozenList = ArrayList(list)
|
||||
return doCalculateDiff(frozenList, newItems)
|
||||
return doCalculateDiff(list, newItems)
|
||||
}
|
||||
|
||||
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
|
||||
@@ -40,47 +31,34 @@ open class DiffObservableList<T>(
|
||||
|
||||
override fun getNewListSize() = newItems.size
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldItems[oldItemPosition]
|
||||
val newItem = newItems[newItemPosition]
|
||||
return callback.areItemsTheSame(oldItem, newItem)
|
||||
return (oldItem as DiffItem<Any>).itemSameAs(newItem)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldItems[oldItemPosition]
|
||||
val newItem = newItems[newItemPosition]
|
||||
return callback.areContentsTheSame(oldItem, newItem)
|
||||
return (oldItem as DiffItem<Any>).contentSameAs(newItem)
|
||||
}
|
||||
}, detectMoves)
|
||||
}, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the contents of this list to the given one using the DiffResults to dispatch change
|
||||
* notifications.
|
||||
*
|
||||
* @param newItems The items to set this list to.
|
||||
* @param diffResult The diff results to dispatch change notifications.
|
||||
*/
|
||||
@MainThread
|
||||
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
||||
list = newItems.toMutableList()
|
||||
diffResult.dispatchUpdatesTo(listCallback)
|
||||
list = ArrayList(newItems)
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this list to the given items. This is a convenience method for calling [ ][.calculateDiff] followed by [.update].
|
||||
*
|
||||
*
|
||||
* **Warning!** If the lists are large this operation may be too slow for the main thread. In
|
||||
* that case, you should call [.calculateDiff] on a background thread and then
|
||||
* [.update] on the main thread.
|
||||
*
|
||||
* @param newItems The items to set this list to.
|
||||
*/
|
||||
@MainThread
|
||||
fun update(newItems: List<T>) {
|
||||
val diffResult = doCalculateDiff(list, newItems)
|
||||
update(newItems, diffResult)
|
||||
@WorkerThread
|
||||
suspend fun update(newItems: List<T>) {
|
||||
val diffResult = calculateDiff(newItems)
|
||||
withContext(Dispatchers.Main) {
|
||||
update(newItems, diffResult)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
||||
@@ -91,113 +69,21 @@ open class DiffObservableList<T>(
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
override fun get(index: Int) = list[index]
|
||||
|
||||
override fun add(index: Int, element: T) {
|
||||
list.add(index, element)
|
||||
notifyAdd(index, 1)
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
listeners.notifyChanged(this, position, count)
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<T>) = addAll(size, elements)
|
||||
|
||||
override fun addAll(index: Int, elements: Collection<T>): Boolean {
|
||||
val added = list.addAll(index, elements)
|
||||
if (added) {
|
||||
notifyAdd(index, elements.size)
|
||||
}
|
||||
return added
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
listeners.notifyMoved(this, fromPosition, toPosition, 1)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
val oldSize = size
|
||||
list.clear()
|
||||
if (oldSize != 0) {
|
||||
notifyRemove(0, oldSize)
|
||||
}
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
modCount += 1
|
||||
listeners.notifyInserted(this, position, count)
|
||||
}
|
||||
|
||||
override fun remove(element: T): Boolean {
|
||||
val index = indexOf(element)
|
||||
return if (index >= 0) {
|
||||
removeAt(index)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeAt(index: Int): T {
|
||||
val element = list.removeAt(index)
|
||||
notifyRemove(index, 1)
|
||||
return element
|
||||
}
|
||||
|
||||
override fun set(index: Int, element: T): T {
|
||||
val old = list.set(index, element)
|
||||
listeners.notifyChanged(this, index, 1)
|
||||
return old
|
||||
}
|
||||
|
||||
private fun notifyAdd(start: Int, count: Int) {
|
||||
listeners.notifyInserted(this, start, count)
|
||||
}
|
||||
|
||||
private fun notifyRemove(start: Int, count: Int) {
|
||||
listeners.notifyRemoved(this, start, count)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Callback class used by DiffUtil while calculating the diff between two lists.
|
||||
*/
|
||||
interface Callback<T> {
|
||||
/**
|
||||
* Called by the DiffUtil to decide whether two object represent the same Item.
|
||||
*
|
||||
*
|
||||
* For example, if your items have unique ids, this method should check their id equality.
|
||||
*
|
||||
* @param oldItem The old item.
|
||||
* @param newItem The new item.
|
||||
* @return True if the two items represent the same object or false if they are different.
|
||||
*/
|
||||
fun areItemsTheSame(oldItem: T, newItem: T): Boolean
|
||||
|
||||
/**
|
||||
* Called by the DiffUtil when it wants to check whether two items have the same data.
|
||||
* DiffUtil uses this information to detect if the contents of an item has changed.
|
||||
*
|
||||
*
|
||||
* DiffUtil uses this method to check equality instead of [Object.equals] so
|
||||
* that you can change its behavior depending on your UI.
|
||||
*
|
||||
*
|
||||
* This method is called only if [.areItemsTheSame] returns `true` for
|
||||
* these items.
|
||||
*
|
||||
* @param oldItem The old item.
|
||||
* @param newItem The new item which replaces the old item.
|
||||
* @return True if the contents of the items are the same or false if they are different.
|
||||
*/
|
||||
fun areContentsTheSame(oldItem: T, newItem: T): Boolean
|
||||
}
|
||||
|
||||
inner class ObservableListUpdateCallback : ListUpdateCallback {
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
listeners.notifyChanged(this@DiffObservableList, position, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
listeners.notifyMoved(this@DiffObservableList, fromPosition, toPosition, 1)
|
||||
}
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
modCount += 1
|
||||
listeners.notifyInserted(this@DiffObservableList, position, count)
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
modCount += 1
|
||||
listeners.notifyRemoved(this@DiffObservableList, position, count)
|
||||
}
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
modCount += 1
|
||||
listeners.notifyRemoved(this, position, count)
|
||||
}
|
||||
}
|
||||
|
@@ -1,83 +1,37 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FilterableDiffObservableList<T>(
|
||||
callback: Callback<T>
|
||||
) : DiffObservableList<T>(callback) {
|
||||
open class FilterableDiffObservableList<T : DiffItem<*>>(
|
||||
private val scope: CoroutineScope
|
||||
) : DiffObservableList<T>() {
|
||||
|
||||
var filter: ((T) -> Boolean)? = null
|
||||
set(value) {
|
||||
field = value
|
||||
queueUpdate()
|
||||
}
|
||||
@Volatile
|
||||
private var sublist: MutableList<T> = super.list
|
||||
private var sublist: List<T> = emptyList()
|
||||
private var job: Job? = null
|
||||
|
||||
// ---
|
||||
|
||||
private val ui by lazy { Handler(Looper.getMainLooper()) }
|
||||
private val handler = Handler(HandlerThread("List${hashCode()}").apply { start() }.looper)
|
||||
private val updater = Runnable {
|
||||
val filter = filter ?: { true }
|
||||
val newList = super.list.filter(filter)
|
||||
val diff = synchronized(this) { doCalculateDiff(sublist, newList) }
|
||||
ui.post {
|
||||
sublist = Collections.synchronizedList(newList)
|
||||
diff.dispatchUpdatesTo(listCallback)
|
||||
fun filter(filter: (T) -> Boolean) {
|
||||
job?.cancel()
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
val oldList = sublist
|
||||
val newList = list.filter(filter)
|
||||
val diff = doCalculateDiff(oldList, newList)
|
||||
withContext(Dispatchers.Main) {
|
||||
sublist = newList
|
||||
diff.dispatchUpdatesTo(this@FilterableDiffObservableList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun queueUpdate() {
|
||||
handler.removeCallbacks(updater)
|
||||
handler.post(updater)
|
||||
}
|
||||
|
||||
fun hasFilter() = filter != null
|
||||
|
||||
fun filter(switch: (T) -> Boolean) {
|
||||
filter = switch
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
filter = null
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun get(index: Int): T {
|
||||
return sublist.get(index)
|
||||
}
|
||||
|
||||
override fun add(element: T): Boolean {
|
||||
return sublist.add(element)
|
||||
}
|
||||
|
||||
override fun add(index: Int, element: T) {
|
||||
sublist.add(index, element)
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<T>): Boolean {
|
||||
return sublist.addAll(elements)
|
||||
}
|
||||
|
||||
override fun addAll(index: Int, elements: Collection<T>): Boolean {
|
||||
return sublist.addAll(index, elements)
|
||||
}
|
||||
|
||||
override fun remove(element: T): Boolean {
|
||||
return sublist.remove(element)
|
||||
}
|
||||
|
||||
override fun removeAt(index: Int): T {
|
||||
return sublist.removeAt(index)
|
||||
}
|
||||
|
||||
override fun set(index: Int, element: T): T {
|
||||
return sublist.set(index, element)
|
||||
return sublist[index]
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
|
@@ -1,7 +0,0 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
fun <T : AnyDiffRvItem> diffListOf() =
|
||||
DiffObservableList(DiffRvItem.callback<T>())
|
||||
|
||||
fun <T : AnyDiffRvItem> filterableListOf() =
|
||||
FilterableDiffObservableList(DiffRvItem.callback<T>())
|
@@ -8,60 +8,28 @@ abstract class RvItem {
|
||||
abstract val layoutRes: Int
|
||||
}
|
||||
|
||||
interface RvContainer<E> {
|
||||
val item: E
|
||||
}
|
||||
|
||||
interface ViewAwareRvItem {
|
||||
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||
}
|
||||
|
||||
interface ComparableRv<T> : Comparable<T> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun comparableEqual(o: Any?) =
|
||||
o != null && o::class == this::class && compareTo(o as T) == 0
|
||||
}
|
||||
|
||||
abstract class DiffRvItem<T> : RvItem() {
|
||||
|
||||
// Defer to contentSameAs by default
|
||||
open fun itemSameAs(other: T) = true
|
||||
|
||||
open fun contentSameAs(other: T) =
|
||||
when (this) {
|
||||
is RvContainer<*> -> item == (other as RvContainer<*>).item
|
||||
is ComparableRv<*> -> comparableEqual(other)
|
||||
else -> this == other
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val callback = object : DiffObservableList.Callback<DiffRvItem<Any>> {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: DiffRvItem<Any>,
|
||||
newItem: DiffRvItem<Any>
|
||||
): Boolean {
|
||||
return oldItem::class == newItem::class && oldItem.itemSameAs(newItem)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: DiffRvItem<Any>,
|
||||
newItem: DiffRvItem<Any>
|
||||
): Boolean {
|
||||
return oldItem.contentSameAs(newItem)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : AnyDiffRvItem> callback() = callback as DiffObservableList.Callback<T>
|
||||
}
|
||||
}
|
||||
|
||||
typealias AnyDiffRvItem = DiffRvItem<*>
|
||||
|
||||
abstract class ObservableDiffRvItem<T> : DiffRvItem<T>(), ObservableHost {
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
}
|
||||
|
||||
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
}
|
||||
|
||||
interface ItemWrapper<E> {
|
||||
val item: E
|
||||
}
|
||||
|
||||
interface ViewAwareItem {
|
||||
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||
}
|
||||
|
||||
interface DiffItem<T : Any> {
|
||||
|
||||
fun itemSameAs(other: T): Boolean {
|
||||
if (this === other) return true
|
||||
return when (this) {
|
||||
is ItemWrapper<*> -> item == (other as ItemWrapper<*>).item
|
||||
is Comparable<*> -> compareValues(this, other as Comparable<*>) == 0
|
||||
else -> this == other
|
||||
}
|
||||
}
|
||||
|
||||
fun contentSameAs(other: T) = true
|
||||
}
|
||||
|
@@ -15,8 +15,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.BR
|
||||
|
||||
class RvItemAdapter<T: RvItem>(
|
||||
private val items: List<T>,
|
||||
private val extraBindings: SparseArray<*>?
|
||||
val items: List<T>,
|
||||
val extraBindings: SparseArray<*>?
|
||||
) : RecyclerView.Adapter<RvItemAdapter.ViewHolder>() {
|
||||
|
||||
private var lifecycleOwner: LifecycleOwner? = null
|
||||
@@ -53,7 +53,7 @@ class RvItemAdapter<T: RvItem>(
|
||||
holder.binding.lifecycleOwner = lifecycleOwner
|
||||
holder.binding.executePendingBindings()
|
||||
recyclerView?.let {
|
||||
if (item is ViewAwareRvItem)
|
||||
if (item is ViewAwareItem)
|
||||
item.onBind(holder.binding, it)
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,9 @@ inline fun bindExtra(body: (SparseArray<Any?>) -> Unit) = SparseArray<Any?>().al
|
||||
@BindingAdapter("items", "extraBindings", requireAll = false)
|
||||
fun <T: RvItem> RecyclerView.setAdapter(items: List<T>?, extraBindings: SparseArray<*>?) {
|
||||
if (items != null) {
|
||||
adapter = RvItemAdapter(items, extraBindings)
|
||||
val rva = (adapter as? RvItemAdapter<*>)
|
||||
if (rva == null || rva.items !== items || rva.extraBindings !== extraBindings) {
|
||||
adapter = RvItemAdapter(items, extraBindings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,14 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.UIActivity
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class DarkThemeDialog : DialogEvent() {
|
||||
class DarkThemeDialog : DialogBuilder {
|
||||
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
val activity = dialog.ownerActivity!!
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
@@ -6,11 +6,12 @@ import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
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 kotlinx.coroutines.launch
|
||||
|
||||
class EnvFixDialog(private val vm: HomeViewModel) : DialogEvent() {
|
||||
class EnvFixDialog(private val vm: HomeViewModel, private val code: Int) : DialogBuilder {
|
||||
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
dialog.apply {
|
||||
@@ -38,8 +39,10 @@ class EnvFixDialog(private val vm: HomeViewModel) : DialogEvent() {
|
||||
}
|
||||
}
|
||||
|
||||
if (Info.env.versionCode != BuildConfig.VERSION_CODE ||
|
||||
if (code == 2 || // No rules block, module policy not loaded
|
||||
Info.env.versionCode != BuildConfig.VERSION_CODE ||
|
||||
Info.env.versionString != BuildConfig.VERSION_NAME) {
|
||||
dialog.setMessage(R.string.env_full_fix_msg)
|
||||
dialog.setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = android.R.string.ok
|
||||
onClick {
|
@@ -0,0 +1,33 @@
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.MainDirections
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class LocalModuleInstallDialog(
|
||||
private val viewModel: ModuleViewModel,
|
||||
private val uri: Uri
|
||||
) : DialogBuilder {
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
dialog.apply {
|
||||
setTitle(R.string.confirm_install_title)
|
||||
setMessage(context.getString(R.string.confirm_install, uri.displayName))
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = android.R.string.ok
|
||||
onClick {
|
||||
viewModel.apply {
|
||||
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, uri).navigate()
|
||||
}
|
||||
}
|
||||
}
|
||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
text = android.R.string.cancel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
@@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() {
|
||||
setCancelable(true)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = R.string.install
|
||||
onClick { DownloadService.start(context, Subject.App()) }
|
||||
onClick { DownloadService.start(activity, Subject.App()) }
|
||||
}
|
||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
text = android.R.string.cancel
|
@@ -1,12 +1,12 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -14,7 +14,7 @@ import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
abstract class MarkDownDialog : DialogEvent() {
|
||||
abstract class MarkDownDialog : DialogBuilder {
|
||||
|
||||
abstract suspend fun getMarkdownText(): String
|
||||
|
||||
@@ -24,7 +24,7 @@ abstract class MarkDownDialog : DialogEvent() {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
|
||||
setView(view)
|
||||
val tv = view.findViewById<TextView>(R.id.md_txt)
|
||||
(ownerActivity as BaseActivity).lifecycleScope.launch {
|
||||
activity.lifecycleScope.launch {
|
||||
try {
|
||||
val text = withContext(Dispatchers.IO) { getMarkdownText() }
|
||||
ServiceLocator.markwon.setMarkdown(tv, text)
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
@@ -8,7 +8,7 @@ import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class ModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
||||
class OnlineModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
||||
|
||||
private val svc get() = ServiceLocator.networkService
|
||||
|
||||
@@ -24,7 +24,7 @@ class ModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
||||
fun download(install: Boolean) {
|
||||
val action = if (install) Action.Flash else Action.Download
|
||||
val subject = Subject.Module(item, action)
|
||||
DownloadService.start(context, subject)
|
||||
DownloadService.start(activity, subject)
|
||||
}
|
||||
|
||||
val title = context.getString(R.string.repo_install_title,
|
@@ -1,9 +1,10 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class SecondSlotWarningDialog : DialogEvent() {
|
||||
class SecondSlotWarningDialog : DialogBuilder {
|
||||
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
dialog.apply {
|
@@ -0,0 +1,25 @@
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class SuperuserRevokeDialog(
|
||||
private val appName: String,
|
||||
private val onSuccess: () -> Unit
|
||||
) : DialogBuilder {
|
||||
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
dialog.apply {
|
||||
setTitle(R.string.su_revoke_title)
|
||||
setMessage(R.string.su_revoke_msg, appName)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = android.R.string.ok
|
||||
onClick { onSuccess() }
|
||||
}
|
||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
text = android.R.string.cancel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,16 +1,17 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
package com.topjohnwu.magisk.dialog
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
class UninstallDialog : DialogEvent() {
|
||||
class UninstallDialog : DialogBuilder {
|
||||
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
dialog.apply {
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
package com.topjohnwu.magisk.events
|
||||
|
||||
import com.topjohnwu.magisk.arch.ActivityExecutor
|
||||
import com.topjohnwu.magisk.arch.UIActivity
|
||||
@@ -9,8 +9,8 @@ class BiometricEvent(
|
||||
builder: Builder.() -> Unit
|
||||
) : ViewEvent(), ActivityExecutor {
|
||||
|
||||
private var listenerOnFailure: GenericDialogListener = {}
|
||||
private var listenerOnSuccess: GenericDialogListener = {}
|
||||
private var listenerOnFailure: () -> Unit = {}
|
||||
private var listenerOnSuccess: () -> Unit = {}
|
||||
|
||||
init {
|
||||
builder(Builder())
|
||||
@@ -26,11 +26,11 @@ class BiometricEvent(
|
||||
|
||||
inner class Builder internal constructor() {
|
||||
|
||||
fun onFailure(listener: GenericDialogListener) {
|
||||
fun onFailure(listener: () -> Unit) {
|
||||
listenerOnFailure = listener
|
||||
}
|
||||
|
||||
fun onSuccess(listener: GenericDialogListener) {
|
||||
fun onSuccess(listener: () -> Unit) {
|
||||
listenerOnSuccess = listener
|
||||
}
|
||||
}
|
@@ -5,10 +5,15 @@ import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.navigation.NavDirections
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.topjohnwu.magisk.arch.*
|
||||
import com.topjohnwu.magisk.arch.ActivityExecutor
|
||||
import com.topjohnwu.magisk.arch.ContextExecutor
|
||||
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||
import com.topjohnwu.magisk.arch.UIActivity
|
||||
import com.topjohnwu.magisk.arch.ViewEvent
|
||||
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.magisk.utils.asText
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
|
||||
class PermissionEvent(
|
||||
@@ -95,3 +100,15 @@ class SnackbarEvent(
|
||||
activity.showSnackbar(msg.getText(activity.resources), length, builder)
|
||||
}
|
||||
}
|
||||
|
||||
class DialogEvent(
|
||||
private val builder: DialogBuilder
|
||||
) : ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
MagiskDialog(activity).apply(builder::build).show()
|
||||
}
|
||||
}
|
||||
|
||||
interface DialogBuilder {
|
||||
fun build(dialog: MagiskDialog)
|
||||
}
|
||||
|
@@ -1,20 +0,0 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
|
||||
import com.topjohnwu.magisk.arch.ActivityExecutor
|
||||
import com.topjohnwu.magisk.arch.UIActivity
|
||||
import com.topjohnwu.magisk.arch.ViewEvent
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
abstract class DialogEvent : ViewEvent(), ActivityExecutor {
|
||||
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
MagiskDialog(activity)
|
||||
.apply { setOwnerActivity(activity) }
|
||||
.apply(this::build).show()
|
||||
}
|
||||
|
||||
abstract fun build(dialog: MagiskDialog)
|
||||
|
||||
}
|
||||
|
||||
typealias GenericDialogListener = () -> Unit
|
@@ -1,35 +0,0 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class SuperuserRevokeDialog(
|
||||
builder: Builder.() -> Unit
|
||||
) : DialogEvent() {
|
||||
|
||||
private val callbacks = Builder().apply(builder)
|
||||
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
dialog.apply {
|
||||
setTitle(R.string.su_revoke_title)
|
||||
setMessage(R.string.su_revoke_msg, callbacks.appName)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = android.R.string.ok
|
||||
onClick { callbacks.listenerOnSuccess() }
|
||||
}
|
||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
text = android.R.string.cancel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class Builder internal constructor() {
|
||||
var appName: String = ""
|
||||
|
||||
internal var listenerOnSuccess: GenericDialogListener = {}
|
||||
|
||||
fun onSuccess(listener: GenericDialogListener) {
|
||||
listenerOnSuccess = listener
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,10 +2,7 @@ package com.topjohnwu.magisk.ktx
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.*
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
@@ -17,6 +14,7 @@ import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Process
|
||||
import android.view.View
|
||||
@@ -32,6 +30,7 @@ import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.io.File
|
||||
import kotlin.Array
|
||||
@@ -44,7 +43,7 @@ fun Context.getBitmap(id: Int): Bitmap {
|
||||
var drawable = AppCompatResources.getDrawable(this, id)!!
|
||||
if (drawable is BitmapDrawable)
|
||||
return drawable.bitmap
|
||||
if (SDK_INT >= 26 && drawable is AdaptiveIconDrawable) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) {
|
||||
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
@@ -58,7 +57,7 @@ fun Context.getBitmap(id: Int): Bitmap {
|
||||
}
|
||||
|
||||
val Context.deviceProtectedContext: Context get() =
|
||||
if (SDK_INT >= 24) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||
createDeviceProtectedStorageContext()
|
||||
} else { this }
|
||||
|
||||
@@ -184,12 +183,8 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||
|
||||
fun Context.unwrap(): Context {
|
||||
var context = this
|
||||
while (true) {
|
||||
if (context is ContextWrapper)
|
||||
context = context.baseContext
|
||||
else
|
||||
break
|
||||
}
|
||||
while (context is ContextWrapper)
|
||||
context = context.baseContext
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -265,3 +260,14 @@ fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
|
||||
}
|
||||
throw PackageManager.NameNotFoundException()
|
||||
}
|
||||
|
||||
fun Context.registerRuntimeReceiver(receiver: BroadcastReceiver, filter: IntentFilter) {
|
||||
APKInstall.registerReceiver(this, receiver, filter)
|
||||
}
|
||||
|
||||
fun Context.selfLaunchIntent(): Intent {
|
||||
val pm = packageManager
|
||||
val intent = pm.getLaunchIntentForPackage(packageName)!!
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
return intent
|
||||
}
|
||||
|
@@ -680,7 +680,7 @@ public abstract class ApkSignerV2 {
|
||||
return "SHA-512";
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown content digest algorthm: " + digestAlgorithm);
|
||||
"Unknown content digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,7 +692,7 @@ public abstract class ApkSignerV2 {
|
||||
return 512 / 8;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown content digest algorthm: " + digestAlgorithm);
|
||||
"Unknown content digest algorithm: " + digestAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package com.topjohnwu.magisk.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Bundle
|
||||
@@ -10,6 +12,7 @@ import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.MainDirections
|
||||
import com.topjohnwu.magisk.R
|
||||
@@ -20,12 +23,15 @@ import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
|
||||
import com.topjohnwu.magisk.ktx.startAnimations
|
||||
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
class MainViewModel : BaseViewModel()
|
||||
@@ -52,10 +58,19 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
||||
|
||||
private var isRootFragment = true
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun showMainUI(savedInstanceState: Bundle?) {
|
||||
setContentView()
|
||||
showUnsupportedMessage()
|
||||
askForHomeShortcut()
|
||||
checkStubComponent()
|
||||
|
||||
// Ask permission to post notifications for background update check
|
||||
if (Config.checkUpdate) {
|
||||
withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||
Config.checkUpdate = it
|
||||
}
|
||||
}
|
||||
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
|
||||
@@ -217,4 +232,22 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun checkStubComponent() {
|
||||
if (intent.component?.className?.contains(HideAPK.PLACEHOLDER) == true) {
|
||||
// The stub APK was not properly patched, re-apply our changes
|
||||
withPermission(Manifest.permission.REQUEST_INSTALL_PACKAGES) { granted ->
|
||||
if (granted) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val apk = File(applicationInfo.sourceDir)
|
||||
HideAPK.upgrade(this@MainActivity, apk)?.let {
|
||||
startActivity(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,9 +6,11 @@ import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
@@ -20,7 +22,6 @@ import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -29,13 +30,15 @@ import kotlinx.coroutines.launch
|
||||
abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
||||
|
||||
companion object {
|
||||
private var skipSplash = false
|
||||
private var splashShown = false
|
||||
}
|
||||
|
||||
private var needShowMainUI = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(Theme.selected.themeRes)
|
||||
|
||||
if (isRunningAsStub && !skipSplash) {
|
||||
if (isRunningAsStub && !splashShown) {
|
||||
// Manually apply splash theme for stub
|
||||
theme.applyStyle(R.style.StubSplashTheme, true)
|
||||
}
|
||||
@@ -44,11 +47,11 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||
|
||||
if (!isRunningAsStub) {
|
||||
val splashScreen = installSplashScreen()
|
||||
splashScreen.setKeepOnScreenCondition { !skipSplash }
|
||||
splashScreen.setKeepOnScreenCondition { !splashShown }
|
||||
}
|
||||
|
||||
if (skipSplash) {
|
||||
showMainUI(savedInstanceState)
|
||||
if (splashShown) {
|
||||
doShowMainUI(savedInstanceState)
|
||||
} else {
|
||||
Shell.getShell(Shell.EXECUTOR) {
|
||||
if (isRunningAsStub && !it.isRoot) {
|
||||
@@ -60,6 +63,11 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||
}
|
||||
}
|
||||
|
||||
private fun doShowMainUI(savedInstanceState: Bundle?) {
|
||||
needShowMainUI = false
|
||||
showMainUI(savedInstanceState)
|
||||
}
|
||||
|
||||
abstract fun showMainUI(savedInstanceState: Bundle?)
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@@ -87,6 +95,13 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (needShowMainUI) {
|
||||
doShowMainUI(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun preLoad(savedState: Bundle?) {
|
||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)?.let {
|
||||
// Make sure the calling package matches (prevent DoS)
|
||||
@@ -98,15 +113,14 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||
|
||||
Config.load(prevPkg)
|
||||
handleRepackage(prevPkg)
|
||||
if (prevPkg != null && !isRunningAsStub) {
|
||||
if (prevPkg != null) {
|
||||
runOnUiThread {
|
||||
// Might have new configuration loaded, relaunch the activity
|
||||
relaunch()
|
||||
// Relaunch the process after package migration
|
||||
StubApk.restartProcess(this)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Notifications.setup(this)
|
||||
JobService.schedule(this)
|
||||
Shortcuts.setupDynamic(this)
|
||||
|
||||
@@ -117,12 +131,16 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||
RootUtils.Connection.await()
|
||||
|
||||
runOnUiThread {
|
||||
skipSplash = true
|
||||
splashShown = true
|
||||
if (isRunningAsStub) {
|
||||
// Re-launch main activity without splash theme
|
||||
relaunch()
|
||||
} else {
|
||||
showMainUI(savedState)
|
||||
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||
doShowMainUI(savedState)
|
||||
} else {
|
||||
needShowMainUI = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,11 +153,10 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||
Shell.cmd("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
} else {
|
||||
if (!Const.Version.atLeast_25_0() && Config.suManager.isNotEmpty())
|
||||
if (Config.suManager.isNotEmpty())
|
||||
Config.suManager = ""
|
||||
pkg ?: return
|
||||
Shell.cmd("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.*
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.core.os.ProcessCompat
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
@@ -67,7 +68,8 @@ class AppProcessInfo(
|
||||
val proc = info.processName ?: info.packageName
|
||||
createProcess("${proc}_zygote")
|
||||
} else {
|
||||
val proc = if (SDK_INT >= 29) "${it.getProcName()}:${it.name}" else it.getProcName()
|
||||
val proc = if (SDK_INT >= Build.VERSION_CODES.Q)
|
||||
"${it.getProcName()}:${it.name}" else it.getProcName()
|
||||
createProcess(proc, ISOLATED_MAGIC)
|
||||
}
|
||||
} else {
|
||||
|
@@ -1,12 +1,12 @@
|
||||
package com.topjohnwu.magisk.ui.deny
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseFragment
|
||||
@@ -17,17 +17,16 @@ import rikka.recyclerview.addEdgeSpacing
|
||||
import rikka.recyclerview.addItemSpacing
|
||||
import rikka.recyclerview.fixEdgeEffect
|
||||
|
||||
class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
||||
class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>(), MenuProvider {
|
||||
|
||||
override val layoutRes = R.layout.fragment_deny_md2
|
||||
override val viewModel by viewModel<DenyListViewModel>()
|
||||
|
||||
private lateinit var searchView: SearchView
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
activity?.setTitle(R.string.denylist)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -56,7 +55,7 @@ class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
||||
return super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_deny_md2, menu)
|
||||
searchView = menu.findItem(R.id.action_search).actionView as SearchView
|
||||
searchView.queryHint = searchView.context.getString(R.string.hide_filter_hint)
|
||||
@@ -73,7 +72,7 @@ class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
||||
})
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_show_system -> {
|
||||
val check = !item.isChecked
|
||||
@@ -91,7 +90,7 @@ class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
val showSystem = menu.findItem(R.id.action_show_system)
|
||||
val showOS = menu.findItem(R.id.action_show_OS)
|
||||
showOS.isEnabled = showSystem.isChecked
|
||||
|
@@ -5,8 +5,8 @@ import android.view.ViewGroup
|
||||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRv
|
||||
import com.topjohnwu.magisk.databinding.ObservableDiffRvItem
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||
import com.topjohnwu.magisk.databinding.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.ktx.startAnimations
|
||||
@@ -15,7 +15,7 @@ import kotlin.math.roundToInt
|
||||
|
||||
class DenyListRvItem(
|
||||
val info: AppProcessInfo
|
||||
) : ObservableDiffRvItem<DenyListRvItem>(), ComparableRv<DenyListRvItem> {
|
||||
) : ObservableRvItem(), DiffItem<DenyListRvItem>, Comparable<DenyListRvItem> {
|
||||
|
||||
override val layoutRes get() = R.layout.item_hide_md2
|
||||
|
||||
@@ -44,9 +44,18 @@ class DenyListRvItem(
|
||||
processes
|
||||
.filterNot { it.isEnabled }
|
||||
.filter { isExpanded || it.defaultSelection }
|
||||
.forEach { it.toggle() }
|
||||
} else {
|
||||
processes.filter { it.isEnabled }
|
||||
}.forEach { it.toggle() }
|
||||
Shell.cmd("magisk --denylist rm ${info.packageName}").submit()
|
||||
processes.filter { it.isEnabled }.forEach {
|
||||
if (it.process.isIsolated) {
|
||||
it.toggle()
|
||||
} else {
|
||||
it.isEnabled = !it.isEnabled
|
||||
notifyPropertyChanged(BR.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -91,7 +100,7 @@ class DenyListRvItem(
|
||||
|
||||
class ProcessRvItem(
|
||||
val process: ProcessInfo
|
||||
) : ObservableDiffRvItem<ProcessRvItem>() {
|
||||
) : ObservableRvItem(), DiffItem<ProcessRvItem> {
|
||||
|
||||
override val layoutRes get() = R.layout.item_hide_process_md2
|
||||
|
||||
@@ -113,10 +122,9 @@ class ProcessRvItem(
|
||||
val defaultSelection get() =
|
||||
process.isIsolated || process.isAppZygote || process.name == process.packageName
|
||||
|
||||
override fun contentSameAs(other: ProcessRvItem) =
|
||||
process.isEnabled == other.process.isEnabled
|
||||
|
||||
override fun itemSameAs(other: ProcessRvItem) =
|
||||
process.name == other.process.name && process.packageName == other.process.packageName
|
||||
|
||||
override fun contentSameAs(other: ProcessRvItem) =
|
||||
process.isEnabled == other.process.isEnabled
|
||||
}
|
||||
|
@@ -2,23 +2,23 @@ package com.topjohnwu.magisk.ui.deny
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.databinding.FilterableDiffObservableList
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.filterableListOf
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.ktx.concurrentMap
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.toCollection
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DenyListViewModel : BaseViewModel() {
|
||||
class DenyListViewModel : AsyncLoadViewModel() {
|
||||
|
||||
var isShowSystem = false
|
||||
set(value) {
|
||||
@@ -38,19 +38,19 @@ class DenyListViewModel : BaseViewModel() {
|
||||
query()
|
||||
}
|
||||
|
||||
val items = filterableListOf<DenyListRvItem>()
|
||||
val items = FilterableDiffObservableList<DenyListRvItem>(viewModelScope)
|
||||
val extraBindings = bindExtra {
|
||||
it.put(BR.viewModel, this)
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var loading = true
|
||||
private set(value) = set(value, field, { field = it }, BR.loading)
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun refresh() = viewModelScope.launch {
|
||||
if (!Utils.showSuperUser()) {
|
||||
state = State.LOADING_FAILED
|
||||
return@launch
|
||||
}
|
||||
state = State.LOADING
|
||||
val (apps, diff) = withContext(Dispatchers.Default) {
|
||||
override suspend fun doLoadWork() {
|
||||
loading = true
|
||||
withContext(Dispatchers.Default) {
|
||||
val pm = AppContext.packageManager
|
||||
val denyList = Shell.cmd("magisk --denylist ls").exec().out
|
||||
.map { CmdlineListItem(it) }
|
||||
@@ -63,13 +63,12 @@ class DenyListViewModel : BaseViewModel() {
|
||||
.toCollection(ArrayList(size))
|
||||
}
|
||||
apps.sort()
|
||||
apps to items.calculateDiff(apps)
|
||||
items.update(apps)
|
||||
}
|
||||
items.update(apps, diff)
|
||||
query()
|
||||
}
|
||||
|
||||
fun query() {
|
||||
private fun query() {
|
||||
items.filter {
|
||||
fun filterSystem() = isShowSystem || !it.info.isSystemApp()
|
||||
|
||||
@@ -84,6 +83,6 @@ class DenyListViewModel : BaseViewModel() {
|
||||
|
||||
(it.isChecked || (filterSystem() && filterOS())) && filterQuery()
|
||||
}
|
||||
state = State.LOADED
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
@@ -6,14 +6,15 @@ import androidx.core.view.updateLayoutParams
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.DiffRvItem
|
||||
import com.topjohnwu.magisk.databinding.RvContainer
|
||||
import com.topjohnwu.magisk.databinding.ViewAwareRvItem
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.ItemWrapper
|
||||
import com.topjohnwu.magisk.databinding.RvItem
|
||||
import com.topjohnwu.magisk.databinding.ViewAwareItem
|
||||
import kotlin.math.max
|
||||
|
||||
class ConsoleItem(
|
||||
override val item: String
|
||||
) : DiffRvItem<ConsoleItem>(), ViewAwareRvItem, RvContainer<String> {
|
||||
) : RvItem(), ViewAwareItem, DiffItem<ConsoleItem>, ItemWrapper<String> {
|
||||
override val layoutRes = R.layout.item_console_md2
|
||||
|
||||
private var parentWidth = -1
|
||||
|
@@ -6,6 +6,8 @@ import android.content.pm.ActivityInfo
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.navigation.NavDeepLinkBuilder
|
||||
import com.topjohnwu.magisk.MainDirections
|
||||
import com.topjohnwu.magisk.R
|
||||
@@ -16,11 +18,13 @@ import com.topjohnwu.magisk.core.cmp
|
||||
import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
|
||||
class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
||||
class FlashFragment : BaseFragment<FragmentFlashMd2Binding>(), MenuProvider {
|
||||
|
||||
override val layoutRes = R.layout.fragment_flash_md2
|
||||
override val viewModel by viewModel<FlashViewModel>()
|
||||
override val snackbarView: View get() = binding.snackbarContainer
|
||||
override val snackbarAnchorView: View?
|
||||
get() = if (binding.restartBtn.isShown) binding.restartBtn else super.snackbarAnchorView
|
||||
|
||||
private var defaultOrientation = -1
|
||||
|
||||
@@ -31,19 +35,30 @@ class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setHasOptionsMenu(true)
|
||||
activity?.setTitle(R.string.flash_screen_title)
|
||||
|
||||
viewModel.subtitle.observe(this) {
|
||||
activity?.supportActionBar?.setSubtitle(it)
|
||||
viewModel.state.observe(this) {
|
||||
activity?.supportActionBar?.setSubtitle(
|
||||
when (it) {
|
||||
FlashViewModel.State.FLASHING -> R.string.flashing
|
||||
FlashViewModel.State.SUCCESS -> R.string.done
|
||||
FlashViewModel.State.FAILED -> R.string.failure
|
||||
}
|
||||
)
|
||||
if (it == FlashViewModel.State.SUCCESS && viewModel.showReboot) {
|
||||
binding.restartBtn.apply {
|
||||
if (!this.isVisible) this.show()
|
||||
if (!this.isFocused) this.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_flash, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
return viewModel.onMenuItemClicked(item)
|
||||
}
|
||||
|
||||
@@ -66,7 +81,7 @@ class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
||||
}
|
||||
|
||||
override fun onKeyEvent(event: KeyEvent): Boolean {
|
||||
return when(event.keyCode) {
|
||||
return when (event.keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_UP,
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> true
|
||||
else -> false
|
||||
@@ -74,7 +89,8 @@ class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
if (viewModel.loading) return true
|
||||
if (viewModel.flashing.value == true)
|
||||
return true
|
||||
return super.onBackPressed()
|
||||
}
|
||||
|
||||
|
@@ -2,8 +2,10 @@ package com.topjohnwu.magisk.ui.flash
|
||||
|
||||
import android.view.MenuItem
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.ObservableArrayList
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
@@ -14,7 +16,6 @@ import com.topjohnwu.magisk.core.tasks.FlashZip
|
||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.databinding.diffListOf
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.ktx.reboot
|
||||
@@ -27,14 +28,19 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class FlashViewModel : BaseViewModel() {
|
||||
|
||||
enum class State {
|
||||
FLASHING, SUCCESS, FAILED
|
||||
}
|
||||
|
||||
private val _state = MutableLiveData(State.FLASHING)
|
||||
val state: LiveData<State> get() = _state
|
||||
val flashing = Transformations.map(state) { it == State.FLASHING }
|
||||
|
||||
@get:Bindable
|
||||
var showReboot = Info.isRooted
|
||||
set(value) = set(value, field, { field = it }, BR.showReboot)
|
||||
|
||||
private val _subtitle = MutableLiveData(R.string.flashing)
|
||||
val subtitle get() = _subtitle as LiveData<Int>
|
||||
|
||||
val items = diffListOf<ConsoleItem>()
|
||||
val items = ObservableArrayList<ConsoleItem>()
|
||||
lateinit var args: FlashFragmentArgs
|
||||
|
||||
private val logItems = mutableListOf<String>().synchronized()
|
||||
@@ -83,11 +89,7 @@ class FlashViewModel : BaseViewModel() {
|
||||
}
|
||||
|
||||
private fun onResult(success: Boolean) {
|
||||
state = if (success) State.LOADED else State.LOADING_FAILED
|
||||
when {
|
||||
success -> _subtitle.postValue(R.string.done)
|
||||
else -> _subtitle.postValue(R.string.failure)
|
||||
}
|
||||
_state.value = if (success) State.SUCCESS else State.FAILED
|
||||
}
|
||||
|
||||
fun onMenuItemClicked(item: MenuItem): Boolean {
|
||||
@@ -100,7 +102,8 @@ class FlashViewModel : BaseViewModel() {
|
||||
private fun savePressed() = withExternalRW {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val name = "magisk_install_log_%s.log".format(
|
||||
System.currentTimeMillis().toTime(timeFormatStandard))
|
||||
System.currentTimeMillis().toTime(timeFormatStandard)
|
||||
)
|
||||
val file = MediaStoreUtils.getFile(name, true)
|
||||
file.uri.outputStream().bufferedWriter().use { writer ->
|
||||
synchronized(logItems) {
|
||||
|
@@ -1,26 +1,30 @@
|
||||
package com.topjohnwu.magisk.ui.home
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.MenuProvider
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseFragment
|
||||
import com.topjohnwu.magisk.arch.viewModel
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
|
||||
import com.topjohnwu.magisk.events.RebootEvent
|
||||
|
||||
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>() {
|
||||
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
|
||||
|
||||
override val layoutRes = R.layout.fragment_home_md2
|
||||
override val viewModel by viewModel<HomeViewModel>()
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
activity?.title = resources.getString(R.string.section_home)
|
||||
setHasOptionsMenu(true)
|
||||
activity?.setTitle(R.string.section_home)
|
||||
DownloadService.observeProgress(this, viewModel::onProgressUpdate)
|
||||
}
|
||||
|
||||
@@ -54,17 +58,17 @@ class HomeFragment : BaseFragment<FragmentHomeMd2Binding>() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_home_md2, menu)
|
||||
if (!Info.isRooted)
|
||||
menu.removeItem(R.id.action_reboot)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_settings ->
|
||||
HomeFragmentDirections.actionHomeFragmentToSettingsFragment().navigate()
|
||||
R.id.action_reboot -> activity?.let { RebootEvent.inflateMenu(it).show() }
|
||||
R.id.action_reboot -> activity?.let { RebootMenu.inflate(it).show() }
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
|
@@ -3,11 +3,14 @@ package com.topjohnwu.magisk.ui.home
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.*
|
||||
import com.topjohnwu.magisk.arch.ActivityExecutor
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.arch.ContextExecutor
|
||||
import com.topjohnwu.magisk.arch.UIActivity
|
||||
import com.topjohnwu.magisk.arch.ViewEvent
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
@@ -15,24 +18,23 @@ import com.topjohnwu.magisk.core.download.Subject.App
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.dialog.EnvFixDialog
|
||||
import com.topjohnwu.magisk.dialog.ManagerInstallDialog
|
||||
import com.topjohnwu.magisk.dialog.UninstallDialog
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
|
||||
import com.topjohnwu.magisk.events.dialog.ManagerInstallDialog
|
||||
import com.topjohnwu.magisk.events.dialog.UninstallDialog
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.asText
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
enum class MagiskState {
|
||||
NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
|
||||
}
|
||||
|
||||
class HomeViewModel(
|
||||
private val svc: NetworkService
|
||||
) : BaseViewModel() {
|
||||
) : AsyncLoadViewModel() {
|
||||
|
||||
enum class State {
|
||||
LOADING, INVALID, OUTDATED, UP_TO_DATE
|
||||
}
|
||||
|
||||
val magiskTitleBarrierIds =
|
||||
intArrayOf(R.id.home_magisk_icon, R.id.home_magisk_title, R.id.home_magisk_button)
|
||||
@@ -43,16 +45,17 @@ class HomeViewModel(
|
||||
var isNoticeVisible = Config.safetyNotice
|
||||
set(value) = set(value, field, { field = it }, BR.noticeVisible)
|
||||
|
||||
val stateMagisk
|
||||
val magiskState
|
||||
get() = when {
|
||||
!Info.env.isActive -> MagiskState.NOT_INSTALLED
|
||||
Info.env.versionCode < BuildConfig.VERSION_CODE -> MagiskState.OBSOLETE
|
||||
else -> MagiskState.UP_TO_DATE
|
||||
Info.isRooted && Info.env.isUnsupported -> State.OUTDATED
|
||||
!Info.env.isActive -> State.INVALID
|
||||
Info.env.versionCode < BuildConfig.VERSION_CODE -> State.OUTDATED
|
||||
else -> State.UP_TO_DATE
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var stateManager = MagiskState.LOADING
|
||||
set(value) = set(value, field, { field = it }, BR.stateManager)
|
||||
var appState = State.LOADING
|
||||
set(value) = set(value, field, { field = it }, BR.appState)
|
||||
|
||||
val magiskInstalledVersion
|
||||
get() = Info.env.run {
|
||||
@@ -68,7 +71,6 @@ class HomeViewModel(
|
||||
|
||||
val managerInstalledVersion
|
||||
get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" +
|
||||
Info.stub?.let { " (${it.version})" }.orEmpty() +
|
||||
if (BuildConfig.DEBUG) " (D)" else ""
|
||||
|
||||
@get:Bindable
|
||||
@@ -83,34 +85,26 @@ class HomeViewModel(
|
||||
private var checkedEnv = false
|
||||
}
|
||||
|
||||
override fun refresh() = viewModelScope.launch {
|
||||
state = State.LOADING
|
||||
override suspend fun doLoadWork() {
|
||||
appState = State.LOADING
|
||||
Info.getRemote(svc)?.apply {
|
||||
state = State.LOADED
|
||||
|
||||
stateManager = when {
|
||||
BuildConfig.VERSION_CODE < magisk.versionCode -> MagiskState.OBSOLETE
|
||||
else -> MagiskState.UP_TO_DATE
|
||||
appState = when {
|
||||
BuildConfig.VERSION_CODE < magisk.versionCode -> State.OUTDATED
|
||||
else -> State.UP_TO_DATE
|
||||
}
|
||||
|
||||
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
|
||||
managerRemoteVersion =
|
||||
("${magisk.version} (${magisk.versionCode}) (${stub.versionCode})" +
|
||||
("${magisk.version} (${magisk.versionCode})" +
|
||||
if (isDebug) " (D)" else "").asText()
|
||||
} ?: run {
|
||||
state = State.LOADING_FAILED
|
||||
appState = State.INVALID
|
||||
managerRemoteVersion = R.string.not_available.asText()
|
||||
}
|
||||
ensureEnv()
|
||||
}
|
||||
|
||||
val showTest = false
|
||||
|
||||
fun onTestPressed() = object : ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
/* Entry point to trigger test events within the app */
|
||||
}
|
||||
}.publish()
|
||||
override fun onNetworkChanged(network: Boolean) = startLoading()
|
||||
|
||||
fun onProgressUpdate(progress: Float, subject: Subject) {
|
||||
if (subject is App)
|
||||
@@ -121,16 +115,16 @@ class HomeViewModel(
|
||||
override fun invoke(context: Context) = Utils.openLink(context, link.toUri())
|
||||
}.publish()
|
||||
|
||||
fun onDeletePressed() = UninstallDialog().publish()
|
||||
fun onDeletePressed() = UninstallDialog().show()
|
||||
|
||||
fun onManagerPressed() = when (state) {
|
||||
State.LOADED -> withExternalRW {
|
||||
fun onManagerPressed() = when (appState) {
|
||||
State.LOADING -> SnackbarEvent(R.string.loading).publish()
|
||||
State.INVALID -> SnackbarEvent(R.string.no_connection).publish()
|
||||
else -> withExternalRW {
|
||||
withInstallPermission {
|
||||
ManagerInstallDialog().publish()
|
||||
ManagerInstallDialog().show()
|
||||
}
|
||||
}
|
||||
State.LOADING -> SnackbarEvent(R.string.loading).publish()
|
||||
else -> SnackbarEvent(R.string.no_connection).publish()
|
||||
}
|
||||
|
||||
fun onMagiskPressed() = withExternalRW {
|
||||
@@ -143,12 +137,19 @@ class HomeViewModel(
|
||||
}
|
||||
|
||||
private suspend fun ensureEnv() {
|
||||
if (MagiskState.NOT_INSTALLED == stateMagisk || checkedEnv) return
|
||||
if (magiskState == State.INVALID || checkedEnv) return
|
||||
val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}"
|
||||
if (!Shell.cmd(cmd).await().isSuccess) {
|
||||
EnvFixDialog(this).publish()
|
||||
val code = Shell.cmd(cmd).await().code
|
||||
if (code != 0) {
|
||||
EnvFixDialog(this, code).show()
|
||||
}
|
||||
checkedEnv = true
|
||||
}
|
||||
|
||||
val showTest = false
|
||||
fun onTestPressed() = object : ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
/* Entry point to trigger test events within the app */
|
||||
}
|
||||
}.publish()
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.events
|
||||
package com.topjohnwu.magisk.ui.home
|
||||
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
@@ -10,7 +10,7 @@ import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.ktx.reboot as systemReboot
|
||||
|
||||
object RebootEvent {
|
||||
object RebootMenu {
|
||||
|
||||
private fun reboot(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
@@ -25,14 +25,14 @@ object RebootEvent {
|
||||
return true
|
||||
}
|
||||
|
||||
fun inflateMenu(activity: BaseActivity): PopupMenu {
|
||||
fun inflate(activity: BaseActivity): PopupMenu {
|
||||
val themeWrapper = ContextThemeWrapper(activity, R.style.Foundation_PopupMenu)
|
||||
val menu = PopupMenu(themeWrapper, activity.findViewById(R.id.action_reboot))
|
||||
activity.menuInflater.inflate(R.menu.menu_reboot, menu.menu)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||
activity.getSystemService<PowerManager>()?.isRebootingUserspaceSupported == true)
|
||||
menu.menu.findItem(R.id.action_reboot_userspace).isVisible = true
|
||||
menu.setOnMenuItemClickListener(::reboot)
|
||||
menu.setOnMenuItemClickListener(RebootMenu::reboot)
|
||||
return menu
|
||||
}
|
||||
|
@@ -22,8 +22,8 @@ import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.dialog.SecondSlotWarningDialog
|
||||
import com.topjohnwu.magisk.events.GetContentEvent
|
||||
import com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog
|
||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -57,7 +57,7 @@ class InstallViewModel(
|
||||
GetContentEvent("*/*", UriCallback()).publish()
|
||||
}
|
||||
R.id.method_inactive_slot -> {
|
||||
SecondSlotWarningDialog().publish()
|
||||
SecondSlotWarningDialog().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,6 @@ class InstallViewModel(
|
||||
R.id.method_inactive_slot -> FlashFragment.flash(true).navigate(true)
|
||||
else -> error("Unknown value")
|
||||
}
|
||||
state = State.LOADING
|
||||
}
|
||||
|
||||
override fun onSaveState(state: Bundle) {
|
||||
|
@@ -5,6 +5,7 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isVisible
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseFragment
|
||||
@@ -16,7 +17,7 @@ import rikka.recyclerview.addEdgeSpacing
|
||||
import rikka.recyclerview.addItemSpacing
|
||||
import rikka.recyclerview.fixEdgeEffect
|
||||
|
||||
class LogFragment : BaseFragment<FragmentLogMd2Binding>() {
|
||||
class LogFragment : BaseFragment<FragmentLogMd2Binding>(), MenuProvider {
|
||||
|
||||
override val layoutRes = R.layout.fragment_log_md2
|
||||
override val viewModel by viewModel<LogViewModel>()
|
||||
@@ -40,8 +41,7 @@ class LogFragment : BaseFragment<FragmentLogMd2Binding>() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setHasOptionsMenu(true)
|
||||
activity?.title = resources.getString(R.string.logs)
|
||||
activity?.setTitle(R.string.logs)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -58,15 +58,14 @@ class LogFragment : BaseFragment<FragmentLogMd2Binding>() {
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_log_md2, menu)
|
||||
actionSave = menu.findItem(R.id.action_save)?.also {
|
||||
it.isVisible = !isMagiskLogVisible
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_save -> viewModel.saveMagiskLog()
|
||||
R.id.action_clear ->
|
||||
|
@@ -1,30 +1,28 @@
|
||||
package com.topjohnwu.magisk.ui.log
|
||||
|
||||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import com.topjohnwu.magisk.databinding.ObservableDiffRvItem
|
||||
import com.topjohnwu.magisk.databinding.RvContainer
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.ktx.timeDateFormat
|
||||
import com.topjohnwu.magisk.ktx.toTime
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.ItemWrapper
|
||||
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||
import com.topjohnwu.magisk.databinding.ViewAwareItem
|
||||
|
||||
class LogRvItem(
|
||||
override val item: SuLog
|
||||
) : ObservableDiffRvItem<LogRvItem>(), RvContainer<SuLog> {
|
||||
override val item: String
|
||||
) : ObservableRvItem(), DiffItem<LogRvItem>, ItemWrapper<String>, ViewAwareItem {
|
||||
|
||||
override val layoutRes = R.layout.item_log_access_md2
|
||||
override val layoutRes = R.layout.item_log_textview
|
||||
|
||||
val date = item.time.toTime(timeDateFormat)
|
||||
|
||||
@get:Bindable
|
||||
var isTop = false
|
||||
set(value) = set(value, field, { field = it }, BR.top)
|
||||
|
||||
@get:Bindable
|
||||
var isBottom = false
|
||||
set(value) = set(value, field, { field = it }, BR.bottom)
|
||||
|
||||
override fun itemSameAs(other: LogRvItem) = item.appName == other.item.appName
|
||||
override fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView) {
|
||||
val view = binding.root as MaterialTextView
|
||||
view.measure(0, 0)
|
||||
val desiredWidth = view.measuredWidth
|
||||
val layoutParams = view.layoutParams
|
||||
layoutParams.width = desiredWidth
|
||||
if (recyclerView.width < desiredWidth) {
|
||||
recyclerView.requestLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,18 @@
|
||||
package com.topjohnwu.magisk.ui.log
|
||||
|
||||
import android.system.Os
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.databinding.DiffObservableList
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.diffListOf
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.ktx.timeFormatStandard
|
||||
@@ -24,7 +25,10 @@ import java.io.FileInputStream
|
||||
|
||||
class LogViewModel(
|
||||
private val repo: LogRepository
|
||||
) : BaseViewModel() {
|
||||
) : AsyncLoadViewModel() {
|
||||
@get:Bindable
|
||||
var loading = true
|
||||
private set(value) = set(value, field, { field = it }, BR.loading)
|
||||
|
||||
// --- empty view
|
||||
|
||||
@@ -33,27 +37,32 @@ class LogViewModel(
|
||||
|
||||
// --- su log
|
||||
|
||||
val items = diffListOf<LogRvItem>()
|
||||
val items = DiffObservableList<SuLogRvItem>()
|
||||
val extraBindings = bindExtra {
|
||||
it.put(BR.viewModel, this)
|
||||
}
|
||||
|
||||
// --- magisk log
|
||||
@get:Bindable
|
||||
var consoleText = " "
|
||||
set(value) = set(value, field, { field = it }, BR.consoleText)
|
||||
val logs = DiffObservableList<LogRvItem>()
|
||||
var magiskLogRaw = " "
|
||||
|
||||
override fun refresh() = viewModelScope.launch {
|
||||
consoleText = repo.fetchMagiskLogs()
|
||||
val (suLogs, diff) = withContext(Dispatchers.Default) {
|
||||
val suLogs = repo.fetchSuLogs().map { LogRvItem(it) }
|
||||
override suspend fun doLoadWork() {
|
||||
loading = true
|
||||
|
||||
val (suLogs, suDiff) = withContext(Dispatchers.Default) {
|
||||
magiskLogRaw = repo.fetchMagiskLogs()
|
||||
val newLogs = magiskLogRaw.split('\n').map { LogRvItem(it) }
|
||||
logs.update(newLogs)
|
||||
val suLogs = repo.fetchSuLogs().map { SuLogRvItem(it) }
|
||||
suLogs to items.calculateDiff(suLogs)
|
||||
}
|
||||
|
||||
items.firstOrNull()?.isTop = false
|
||||
items.lastOrNull()?.isBottom = false
|
||||
items.update(suLogs, diff)
|
||||
items.update(suLogs, suDiff)
|
||||
items.firstOrNull()?.isTop = true
|
||||
items.lastOrNull()?.isBottom = true
|
||||
loading = false
|
||||
}
|
||||
|
||||
fun saveMagiskLog() = withExternalRW {
|
||||
@@ -66,17 +75,22 @@ class LogViewModel(
|
||||
file.write("isAB=${Info.isAB}\n")
|
||||
file.write("isSAR=${Info.isSAR}\n")
|
||||
file.write("ramdisk=${Info.ramdisk}\n")
|
||||
val uname = Os.uname()
|
||||
file.write("kernel=${uname.sysname} ${uname.machine} ${uname.release} ${uname.version}\n")
|
||||
|
||||
file.write("\n\n---System Properties---\n\n")
|
||||
ProcessBuilder("getprop").start()
|
||||
.inputStream.reader().use { it.copyTo(file) }
|
||||
|
||||
file.write("\n\n---Environment Variables---\n\n")
|
||||
System.getenv().forEach { (key, value) -> file.write("${key}=${value}\n") }
|
||||
|
||||
file.write("\n\n---System MountInfo---\n\n")
|
||||
FileInputStream("/proc/self/mountinfo").reader().use { it.copyTo(file) }
|
||||
|
||||
file.write("\n---Magisk Logs---\n")
|
||||
file.write("${Info.env.versionString} (${Info.env.versionCode})\n\n")
|
||||
file.write(consoleText)
|
||||
if (Info.env.isActive) file.write(magiskLogRaw)
|
||||
|
||||
file.write("\n---Manager Logs---\n")
|
||||
file.write("${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\n\n")
|
||||
@@ -89,12 +103,12 @@ class LogViewModel(
|
||||
|
||||
fun clearMagiskLog() = repo.clearMagiskLogs {
|
||||
SnackbarEvent(R.string.logs_cleared).publish()
|
||||
requestRefresh()
|
||||
startLoading()
|
||||
}
|
||||
|
||||
fun clearLog() = viewModelScope.launch {
|
||||
repo.clearLogs()
|
||||
SnackbarEvent(R.string.logs_cleared).publish()
|
||||
requestRefresh()
|
||||
startLoading()
|
||||
}
|
||||
}
|
||||
|
28
app/src/main/java/com/topjohnwu/magisk/ui/log/SuLogRvItem.kt
Normal file
28
app/src/main/java/com/topjohnwu/magisk/ui/log/SuLogRvItem.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.topjohnwu.magisk.ui.log
|
||||
|
||||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.ktx.timeDateFormat
|
||||
import com.topjohnwu.magisk.ktx.toTime
|
||||
|
||||
class SuLogRvItem(val log: SuLog) : ObservableRvItem(), DiffItem<SuLogRvItem> {
|
||||
|
||||
override val layoutRes = R.layout.item_log_access_md2
|
||||
|
||||
val date = log.time.toTime(timeDateFormat)
|
||||
|
||||
@get:Bindable
|
||||
var isTop = false
|
||||
set(value) = set(value, field, { field = it }, BR.top)
|
||||
|
||||
@get:Bindable
|
||||
var isBottom = false
|
||||
set(value) = set(value, field, { field = it }, BR.bottom)
|
||||
|
||||
override fun itemSameAs(other: SuLogRvItem) = log.appName == other.log.appName
|
||||
}
|
@@ -2,11 +2,9 @@ package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.topjohnwu.magisk.MainDirections
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseFragment
|
||||
import com.topjohnwu.magisk.arch.viewModel
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
|
||||
import rikka.recyclerview.addEdgeSpacing
|
||||
import rikka.recyclerview.addInvalidateItemDecorationsObserver
|
||||
@@ -23,7 +21,7 @@ class ModuleFragment : BaseFragment<FragmentModuleMd2Binding>() {
|
||||
activity?.title = resources.getString(R.string.modules)
|
||||
viewModel.data.observe(this) {
|
||||
it ?: return@observe
|
||||
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, it).navigate()
|
||||
viewModel.requestInstallLocalModule(it)
|
||||
viewModel.data.value = null
|
||||
}
|
||||
}
|
||||
|
@@ -5,20 +5,21 @@ import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.databinding.DiffRvItem
|
||||
import com.topjohnwu.magisk.databinding.ObservableDiffRvItem
|
||||
import com.topjohnwu.magisk.databinding.RvContainer
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.ItemWrapper
|
||||
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||
import com.topjohnwu.magisk.databinding.RvItem
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.magisk.utils.asText
|
||||
|
||||
object InstallModule : DiffRvItem<InstallModule>() {
|
||||
object InstallModule : RvItem(), DiffItem<InstallModule> {
|
||||
override val layoutRes = R.layout.item_module_download
|
||||
}
|
||||
|
||||
class LocalModuleRvItem(
|
||||
override val item: LocalModule
|
||||
) : ObservableDiffRvItem<LocalModuleRvItem>(), RvContainer<LocalModule> {
|
||||
) : ObservableRvItem(), DiffItem<LocalModuleRvItem>, ItemWrapper<LocalModule> {
|
||||
|
||||
override val layoutRes = R.layout.item_module_md2
|
||||
|
||||
|
@@ -1,33 +1,33 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.databinding.DiffObservableList
|
||||
import com.topjohnwu.magisk.databinding.MergeObservableList
|
||||
import com.topjohnwu.magisk.databinding.RvItem
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.diffListOf
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.dialog.LocalModuleInstallDialog
|
||||
import com.topjohnwu.magisk.dialog.OnlineModuleInstallDialog
|
||||
import com.topjohnwu.magisk.events.GetContentEvent
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class ModuleViewModel : BaseViewModel() {
|
||||
class ModuleViewModel : AsyncLoadViewModel() {
|
||||
|
||||
val bottomBarBarrierIds = intArrayOf(R.id.module_update, R.id.module_remove)
|
||||
|
||||
private val itemsInstalled = diffListOf<LocalModuleRvItem>()
|
||||
private val itemsInstalled = DiffObservableList<LocalModuleRvItem>()
|
||||
|
||||
val items = MergeObservableList<RvItem>()
|
||||
val extraBindings = bindExtra {
|
||||
@@ -36,28 +36,32 @@ class ModuleViewModel : BaseViewModel() {
|
||||
|
||||
val data get() = uri
|
||||
|
||||
init {
|
||||
if (Info.env.isActive && LocalModule.loaded()) {
|
||||
items.insertItem(InstallModule)
|
||||
.insertList(itemsInstalled)
|
||||
@get:Bindable
|
||||
var loading = true
|
||||
private set(value) = set(value, field, { field = it }, BR.loading)
|
||||
|
||||
override suspend fun doLoadWork() {
|
||||
loading = true
|
||||
val moduleLoaded = Info.env.isActive &&
|
||||
withContext(Dispatchers.IO) { LocalModule.loaded() }
|
||||
if (moduleLoaded) {
|
||||
loadInstalled()
|
||||
if (items.isEmpty()) {
|
||||
items.insertItem(InstallModule)
|
||||
.insertList(itemsInstalled)
|
||||
}
|
||||
}
|
||||
loading = false
|
||||
loadUpdateInfo()
|
||||
}
|
||||
|
||||
override fun refresh(): Job {
|
||||
return viewModelScope.launch {
|
||||
state = State.LOADING
|
||||
loadInstalled()
|
||||
state = State.LOADED
|
||||
loadUpdateInfo()
|
||||
}
|
||||
}
|
||||
override fun onNetworkChanged(network: Boolean) = startLoading()
|
||||
|
||||
private suspend fun loadInstalled() {
|
||||
val installed = LocalModule.installed().map { LocalModuleRvItem(it) }
|
||||
val diff = withContext(Dispatchers.Default) {
|
||||
itemsInstalled.calculateDiff(installed)
|
||||
withContext(Dispatchers.Default) {
|
||||
val installed = LocalModule.installed().map { LocalModuleRvItem(it) }
|
||||
itemsInstalled.update(installed)
|
||||
}
|
||||
itemsInstalled.update(installed, diff)
|
||||
}
|
||||
|
||||
private suspend fun loadUpdateInfo() {
|
||||
@@ -71,7 +75,7 @@ class ModuleViewModel : BaseViewModel() {
|
||||
|
||||
fun downloadPressed(item: OnlineModule?) =
|
||||
if (item != null && Info.isConnected.value == true) {
|
||||
withExternalRW { ModuleInstallDialog(item).publish() }
|
||||
withExternalRW { OnlineModuleInstallDialog(item).show() }
|
||||
} else {
|
||||
SnackbarEvent(R.string.no_connection).publish()
|
||||
}
|
||||
@@ -80,6 +84,10 @@ class ModuleViewModel : BaseViewModel() {
|
||||
GetContentEvent("application/zip", UriCallback()).publish()
|
||||
}
|
||||
|
||||
fun requestInstallLocalModule(uri: Uri) {
|
||||
LocalModuleInstallDialog(this, uri).show()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class UriCallback : ContentResultCallback {
|
||||
override fun onActivityResult(result: Uri) {
|
||||
|
@@ -8,6 +8,7 @@ import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.ktx.activity
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
@@ -57,6 +58,8 @@ sealed class BaseSettingsItem : ObservableRvItem() {
|
||||
set(checked, value, { onPressed(view, handler) })
|
||||
|
||||
override fun onPressed(view: View, handler: Handler) {
|
||||
// Make sure the checked state is synced
|
||||
notifyPropertyChanged(BR.checked)
|
||||
handler.onItemPressed(view, this) {
|
||||
value = !value
|
||||
notifyPropertyChanged(BR.checked)
|
||||
@@ -72,7 +75,7 @@ sealed class BaseSettingsItem : ObservableRvItem() {
|
||||
|
||||
override fun onPressed(view: View, handler: Handler) {
|
||||
handler.onItemPressed(view, this) {
|
||||
MagiskDialog(view.context).apply {
|
||||
MagiskDialog(view.activity).apply {
|
||||
setTitle(title.getText(view.resources))
|
||||
setView(getView(view.context))
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
@@ -115,7 +118,7 @@ sealed class BaseSettingsItem : ObservableRvItem() {
|
||||
|
||||
override fun onPressed(view: View, handler: Handler) {
|
||||
handler.onItemPressed(view, this) {
|
||||
MagiskDialog(view.context).apply {
|
||||
MagiskDialog(view.activity).apply {
|
||||
setTitle(title.getText(view.resources))
|
||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
text = android.R.string.cancel
|
||||
|
@@ -12,8 +12,6 @@ import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.JobService
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
@@ -23,6 +21,7 @@ import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
|
||||
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
|
||||
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.ktx.activity
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.asText
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
@@ -111,7 +110,7 @@ object Restore : BaseSettingsItem.Blank() {
|
||||
|
||||
override fun onPressed(view: View, handler: Handler) {
|
||||
handler.onItemPressed(view, this) {
|
||||
MagiskDialog(view.context).apply {
|
||||
MagiskDialog(view.activity).apply {
|
||||
setTitle(R.string.settings_restore_app_title)
|
||||
setMessage(R.string.restore_app_confirmation)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
@@ -201,12 +200,7 @@ object UpdateChannelUrl : BaseSettingsItem.Input() {
|
||||
object UpdateChecker : BaseSettingsItem.Toggle() {
|
||||
override val title = R.string.settings_check_update_title.asText()
|
||||
override val description = R.string.settings_check_update_summary.asText()
|
||||
override var value
|
||||
get() = Config.checkUpdate
|
||||
set(value) {
|
||||
Config.checkUpdate = value
|
||||
JobService.schedule(AppContext)
|
||||
}
|
||||
override var value by Config::checkUpdate
|
||||
}
|
||||
|
||||
object DoHToggle : BaseSettingsItem.Toggle() {
|
||||
|
@@ -16,8 +16,8 @@ import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.events.AddHomeIconEvent
|
||||
import com.topjohnwu.magisk.events.BiometricEvent
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.events.dialog.BiometricEvent
|
||||
import com.topjohnwu.magisk.ktx.activity
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
@@ -53,7 +53,7 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
|
||||
AppSettings,
|
||||
UpdateChannel, UpdateChannelUrl, DoHToggle, UpdateChecker, DownloadPath
|
||||
))
|
||||
if (Build.VERSION.SDK_INT >= 22 && Info.env.isActive && Const.USER_ID == 0) {
|
||||
if (Info.env.isActive && Const.USER_ID == 0) {
|
||||
if (hidden) list.add(Restore) else list.add(Hide)
|
||||
}
|
||||
|
||||
@@ -75,15 +75,11 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
|
||||
Tapjack, Biometrics, AccessMode, MultiuserMode, MountNamespaceMode,
|
||||
AutomaticResponse, RequestTimeout, SUNotification
|
||||
))
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
// Biometric is only available on 6.0+
|
||||
list.remove(Biometrics)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 26) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
// Re-authenticate is not feasible on 8.0+
|
||||
list.add(Reauthenticate)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// Can hide overlay windows on 12.0+
|
||||
list.remove(Tapjack)
|
||||
}
|
||||
@@ -95,6 +91,7 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
|
||||
override fun onItemPressed(view: View, item: BaseSettingsItem, andThen: () -> Unit) {
|
||||
when (item) {
|
||||
DownloadPath -> withExternalRW(andThen)
|
||||
UpdateChecker -> withPostNotificationPermission(andThen)
|
||||
Biometrics -> authenticate(andThen)
|
||||
Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().navigate()
|
||||
DenyListConfig -> SettingsFragmentDirections.actionSettingsFragmentToDenyFragment().navigate()
|
||||
|
@@ -5,8 +5,9 @@ import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.databinding.ObservableDiffRvItem
|
||||
import com.topjohnwu.magisk.databinding.RvContainer
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.ItemWrapper
|
||||
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
|
||||
class PolicyRvItem(
|
||||
@@ -16,7 +17,7 @@ class PolicyRvItem(
|
||||
private val isSharedUid: Boolean,
|
||||
val icon: Drawable,
|
||||
val appName: String
|
||||
) : ObservableDiffRvItem<PolicyRvItem>(), RvContainer<SuPolicy> {
|
||||
) : ObservableRvItem(), DiffItem<PolicyRvItem>, ItemWrapper<SuPolicy> {
|
||||
|
||||
override val layoutRes = R.layout.item_policy_md2
|
||||
|
||||
@@ -36,6 +37,7 @@ class PolicyRvItem(
|
||||
var isEnabled
|
||||
get() = item.policy == SuPolicy.ALLOW
|
||||
set(value) = setImpl(value, isEnabled) {
|
||||
notifyPropertyChanged(BR.enabled)
|
||||
viewModel.togglePolicy(this, value)
|
||||
}
|
||||
|
||||
|
@@ -4,23 +4,21 @@ import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
import android.os.Process
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.ObservableArrayList
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.databinding.AnyDiffRvItem
|
||||
import com.topjohnwu.magisk.databinding.MergeObservableList
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.diffListOf
|
||||
import com.topjohnwu.magisk.databinding.*
|
||||
import com.topjohnwu.magisk.dialog.SuperuserRevokeDialog
|
||||
import com.topjohnwu.magisk.events.BiometricEvent
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.events.dialog.BiometricEvent
|
||||
import com.topjohnwu.magisk.events.dialog.SuperuserRevokeDialog
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.asText
|
||||
@@ -31,30 +29,32 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
class SuperuserViewModel(
|
||||
private val db: PolicyDao
|
||||
) : BaseViewModel() {
|
||||
) : AsyncLoadViewModel() {
|
||||
|
||||
private val itemNoData = TextItem(R.string.superuser_policy_none)
|
||||
|
||||
private val itemsHelpers = ObservableArrayList<TextItem>()
|
||||
private val itemsPolicies = diffListOf<PolicyRvItem>()
|
||||
private val itemsPolicies = DiffObservableList<PolicyRvItem>()
|
||||
|
||||
val items = MergeObservableList<AnyDiffRvItem>()
|
||||
val items = MergeObservableList<RvItem>()
|
||||
.insertList(itemsHelpers)
|
||||
.insertList(itemsPolicies)
|
||||
val extraBindings = bindExtra {
|
||||
it.put(BR.listener, this)
|
||||
}
|
||||
|
||||
// ---
|
||||
@get:Bindable
|
||||
var loading = true
|
||||
private set(value) = set(value, field, { field = it }, BR.loading)
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun refresh() = viewModelScope.launch {
|
||||
override suspend fun doLoadWork() {
|
||||
if (!Utils.showSuperUser()) {
|
||||
state = State.LOADING_FAILED
|
||||
return@launch
|
||||
loading = false
|
||||
return
|
||||
}
|
||||
state = State.LOADING
|
||||
val (policies, diff) = withContext(Dispatchers.IO) {
|
||||
loading = true
|
||||
withContext(Dispatchers.IO) {
|
||||
db.deleteOutdated()
|
||||
db.delete(AppContext.applicationInfo.uid)
|
||||
val policies = ArrayList<PolicyRvItem>()
|
||||
@@ -91,14 +91,13 @@ class SuperuserViewModel(
|
||||
{ it.appName.lowercase(currentLocale) },
|
||||
{ it.packageName }
|
||||
))
|
||||
policies to itemsPolicies.calculateDiff(policies)
|
||||
itemsPolicies.update(policies)
|
||||
}
|
||||
itemsPolicies.update(policies, diff)
|
||||
if (itemsPolicies.isNotEmpty())
|
||||
itemsHelpers.clear()
|
||||
else if (itemsHelpers.isEmpty())
|
||||
itemsHelpers.add(itemNoData)
|
||||
state = State.LOADED
|
||||
loading = false
|
||||
}
|
||||
|
||||
// ---
|
||||
@@ -117,10 +116,7 @@ class SuperuserViewModel(
|
||||
onSuccess { updateState() }
|
||||
}.publish()
|
||||
} else {
|
||||
SuperuserRevokeDialog {
|
||||
appName = item.title
|
||||
onSuccess { updateState() }
|
||||
}.publish()
|
||||
SuperuserRevokeDialog(item.title) { updateState() }.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -24,13 +24,9 @@ open class SuRequestActivity : UIActivity<ActivityRequestBinding>() {
|
||||
override val layoutRes: Int = R.layout.activity_request
|
||||
override val viewModel: SuRequestViewModel by viewModel()
|
||||
|
||||
override fun onBackPressed() {
|
||||
viewModel.denyPressed()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
lockOrientation()
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
window.addFlags(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
@@ -62,7 +58,11 @@ open class SuRequestActivity : UIActivity<ActivityRequestBinding>() {
|
||||
return theme
|
||||
}
|
||||
|
||||
private fun lockOrientation() {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
override fun onBackPressed() {
|
||||
viewModel.denyPressed()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
@@ -27,9 +27,9 @@ import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.DENY
|
||||
import com.topjohnwu.magisk.core.su.SuRequestHandler
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.events.BiometricEvent
|
||||
import com.topjohnwu.magisk.events.DieEvent
|
||||
import com.topjohnwu.magisk.events.ShowUIEvent
|
||||
import com.topjohnwu.magisk.events.dialog.BiometricEvent
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
@@ -189,15 +189,15 @@ class SuRequestViewModel(
|
||||
|
||||
// Invisible for accessibility services
|
||||
object EmptyAccessibilityDelegate : View.AccessibilityDelegate() {
|
||||
override fun sendAccessibilityEvent(host: View?, eventType: Int) {}
|
||||
override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?) = true
|
||||
override fun sendAccessibilityEventUnchecked(host: View?, event: AccessibilityEvent?) {}
|
||||
override fun dispatchPopulateAccessibilityEvent(host: View?, event: AccessibilityEvent?) = true
|
||||
override fun onPopulateAccessibilityEvent(host: View?, event: AccessibilityEvent?) {}
|
||||
override fun onInitializeAccessibilityEvent(host: View?, event: AccessibilityEvent?) {}
|
||||
override fun sendAccessibilityEvent(host: View, eventType: Int) {}
|
||||
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?) = true
|
||||
override fun sendAccessibilityEventUnchecked(host: View, event: AccessibilityEvent) {}
|
||||
override fun dispatchPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) = true
|
||||
override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {}
|
||||
override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent) {}
|
||||
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {}
|
||||
override fun addExtraDataToAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo, extraDataKey: String, arguments: Bundle?) {}
|
||||
override fun onRequestSendAccessibilityEvent(host: ViewGroup?, child: View?, event: AccessibilityEvent?): Boolean = false
|
||||
override fun getAccessibilityNodeProvider(host: View?): AccessibilityNodeProvider? = null
|
||||
override fun onRequestSendAccessibilityEvent(host: ViewGroup, child: View, event: AccessibilityEvent): Boolean = false
|
||||
override fun getAccessibilityNodeProvider(host: View): AccessibilityNodeProvider? = null
|
||||
}
|
||||
}
|
||||
|
@@ -2,8 +2,8 @@ package com.topjohnwu.magisk.ui.theme
|
||||
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.dialog.DarkThemeDialog
|
||||
import com.topjohnwu.magisk.events.RecreateEvent
|
||||
import com.topjohnwu.magisk.events.dialog.DarkThemeDialog
|
||||
import com.topjohnwu.magisk.view.TappableHeadlineItem
|
||||
|
||||
class ThemeViewModel : BaseViewModel(), TappableHeadlineItem.Listener {
|
||||
@@ -11,7 +11,7 @@ class ThemeViewModel : BaseViewModel(), TappableHeadlineItem.Listener {
|
||||
val themeHeadline = TappableHeadlineItem.ThemeMode
|
||||
|
||||
override fun onItemPressed(item: TappableHeadlineItem) = when (item) {
|
||||
is TappableHeadlineItem.ThemeMode -> DarkThemeDialog().publish()
|
||||
is TappableHeadlineItem.ThemeMode -> DarkThemeDialog().show()
|
||||
}
|
||||
|
||||
fun saveTheme(theme: Theme) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk.view
|
||||
|
||||
import android.content.Context
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.Drawable
|
||||
@@ -21,22 +21,33 @@ import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.*
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.databinding.DialogMagiskBaseBinding
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.ItemWrapper
|
||||
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||
import com.topjohnwu.magisk.databinding.RvItem
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.databinding.setAdapter
|
||||
import com.topjohnwu.magisk.view.MagiskDialog.DialogClickListener
|
||||
|
||||
typealias DialogButtonClickListener = (DialogInterface) -> Unit
|
||||
|
||||
class MagiskDialog(
|
||||
context: Context, theme: Int = 0
|
||||
context: Activity, theme: Int = 0
|
||||
) : AppCompatDialog(context, theme) {
|
||||
|
||||
private val binding: DialogMagiskBaseBinding =
|
||||
DialogMagiskBaseBinding.inflate(LayoutInflater.from(context))
|
||||
private val data = Data()
|
||||
|
||||
val activity: BaseActivity get() = ownerActivity as BaseActivity
|
||||
|
||||
init {
|
||||
binding.setVariable(BR.data, data)
|
||||
setCancelable(true)
|
||||
setOwnerActivity(context)
|
||||
}
|
||||
|
||||
inner class Data : ObservableHost {
|
||||
@@ -162,7 +173,7 @@ class MagiskDialog(
|
||||
class DialogItem(
|
||||
override val item: CharSequence,
|
||||
val position: Int
|
||||
) : DiffRvItem<DialogItem>(), RvContainer<CharSequence> {
|
||||
) : RvItem(), DiffItem<DialogItem>, ItemWrapper<CharSequence> {
|
||||
override val layoutRes = R.layout.item_list_single_line
|
||||
}
|
||||
|
||||
|
@@ -5,8 +5,7 @@ import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toIcon
|
||||
@@ -15,6 +14,7 @@ import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.ktx.getBitmap
|
||||
import com.topjohnwu.magisk.ktx.selfLaunchIntent
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -22,83 +22,82 @@ object Notifications {
|
||||
|
||||
val mgr by lazy { AppContext.getSystemService<NotificationManager>()!! }
|
||||
|
||||
private const val APP_UPDATED_NOTIFICATION_ID = 4
|
||||
private const val APP_UPDATE_NOTIFICATION_ID = 5
|
||||
private const val APP_UPDATED_ID = 4
|
||||
private const val APP_UPDATE_AVAILABLE_ID = 5
|
||||
|
||||
private const val UPDATE_CHANNEL = "update"
|
||||
private const val PROGRESS_CHANNEL = "progress"
|
||||
private const val UPDATED_CHANNEL = "updated"
|
||||
|
||||
private val nextId = AtomicInteger(APP_UPDATE_NOTIFICATION_ID)
|
||||
private val nextId = AtomicInteger(APP_UPDATE_AVAILABLE_ID)
|
||||
|
||||
fun setup(context: Context) {
|
||||
if (SDK_INT >= 26) {
|
||||
val channel = NotificationChannel(UPDATE_CHANNEL,
|
||||
context.getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT)
|
||||
val channel2 = NotificationChannel(PROGRESS_CHANNEL,
|
||||
context.getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
|
||||
val channel3 = NotificationChannel(UPDATED_CHANNEL,
|
||||
context.getString(R.string.updated_channel), NotificationManager.IMPORTANCE_HIGH)
|
||||
mgr.createNotificationChannels(listOf(channel, channel2, channel3))
|
||||
fun setup() {
|
||||
AppContext.apply {
|
||||
if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(UPDATE_CHANNEL,
|
||||
getString(R.string.update_channel), NotificationManager.IMPORTANCE_DEFAULT)
|
||||
val channel2 = NotificationChannel(PROGRESS_CHANNEL,
|
||||
getString(R.string.progress_channel), NotificationManager.IMPORTANCE_LOW)
|
||||
val channel3 = NotificationChannel(UPDATED_CHANNEL,
|
||||
getString(R.string.updated_channel), NotificationManager.IMPORTANCE_HIGH)
|
||||
mgr.createNotificationChannels(listOf(channel, channel2, channel3))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selfLaunchIntent(context: Context): Intent {
|
||||
val pm = context.packageManager
|
||||
val intent = pm.getLaunchIntentForPackage(context.packageName)!!
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
return intent
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun updateDone(context: Context) {
|
||||
setup(context)
|
||||
val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
val pending = PendingIntent.getActivity(context, 0, selfLaunchIntent(context), flag)
|
||||
val builder = if (SDK_INT >= 26) {
|
||||
Notification.Builder(context, UPDATED_CHANNEL)
|
||||
.setSmallIcon(context.getBitmap(R.drawable.ic_magisk_outline).toIcon())
|
||||
} else {
|
||||
Notification.Builder(context).setPriority(Notification.PRIORITY_HIGH)
|
||||
.setSmallIcon(R.drawable.ic_magisk_outline)
|
||||
fun updateDone() {
|
||||
AppContext.apply {
|
||||
val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
val pending = PendingIntent.getActivity(this, 0, selfLaunchIntent(), flag)
|
||||
val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, UPDATED_CHANNEL)
|
||||
.setSmallIcon(getBitmap(R.drawable.ic_magisk_outline).toIcon())
|
||||
} else {
|
||||
Notification.Builder(this).setPriority(Notification.PRIORITY_HIGH)
|
||||
.setSmallIcon(R.drawable.ic_magisk_outline)
|
||||
}
|
||||
.setContentIntent(pending)
|
||||
.setContentTitle(getText(R.string.updated_title))
|
||||
.setContentText(getText(R.string.updated_text))
|
||||
.setAutoCancel(true)
|
||||
mgr.notify(APP_UPDATED_ID, builder.build())
|
||||
}
|
||||
.setContentIntent(pending)
|
||||
.setContentTitle(context.getText(R.string.updated_title))
|
||||
.setContentText(context.getText(R.string.updated_text))
|
||||
.setAutoCancel(true)
|
||||
mgr.notify(APP_UPDATED_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun updateAvailable(context: Context) {
|
||||
val intent = DownloadService.getPendingIntent(context, Subject.App())
|
||||
fun updateAvailable() {
|
||||
AppContext.apply {
|
||||
val intent = DownloadService.getPendingIntent(this, Subject.App())
|
||||
val bitmap = getBitmap(R.drawable.ic_magisk_outline)
|
||||
val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, UPDATE_CHANNEL)
|
||||
.setSmallIcon(bitmap.toIcon())
|
||||
} else {
|
||||
Notification.Builder(this)
|
||||
.setSmallIcon(R.drawable.ic_magisk_outline)
|
||||
}
|
||||
.setLargeIcon(bitmap)
|
||||
.setContentTitle(getString(R.string.magisk_update_title))
|
||||
.setContentText(getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(intent)
|
||||
|
||||
val bitmap = context.getBitmap(R.drawable.ic_magisk_outline)
|
||||
val builder = if (SDK_INT >= 26) {
|
||||
Notification.Builder(context, UPDATE_CHANNEL)
|
||||
.setSmallIcon(bitmap.toIcon())
|
||||
} else {
|
||||
Notification.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_magisk_outline)
|
||||
mgr.notify(APP_UPDATE_AVAILABLE_ID, builder.build())
|
||||
}
|
||||
.setLargeIcon(bitmap)
|
||||
.setContentTitle(context.getString(R.string.magisk_update_title))
|
||||
.setContentText(context.getString(R.string.manager_download_install))
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(intent)
|
||||
|
||||
mgr.notify(APP_UPDATE_NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
fun progress(context: Context, title: CharSequence): Notification.Builder {
|
||||
val builder = if (SDK_INT >= 26) {
|
||||
Notification.Builder(context, PROGRESS_CHANNEL)
|
||||
fun startProgress(title: CharSequence): Notification.Builder {
|
||||
val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(AppContext, PROGRESS_CHANNEL)
|
||||
} else {
|
||||
Notification.Builder(context).setPriority(Notification.PRIORITY_LOW)
|
||||
Notification.Builder(AppContext).setPriority(Notification.PRIORITY_LOW)
|
||||
}
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setContentTitle(title)
|
||||
.setProgress(0, 0, true)
|
||||
.setOngoing(true)
|
||||
if (SDK_INT >= Build.VERSION_CODES.S)
|
||||
builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
|
||||
return builder
|
||||
}
|
||||
|
||||
|
@@ -19,7 +19,7 @@ import com.topjohnwu.magisk.utils.Utils
|
||||
object Shortcuts {
|
||||
|
||||
fun setupDynamic(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= 25) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
val manager = context.getSystemService<ShortcutManager>() ?: return
|
||||
manager.dynamicShortcuts = getShortCuts(context)
|
||||
}
|
||||
@@ -36,13 +36,12 @@ object Shortcuts {
|
||||
}
|
||||
|
||||
private fun Context.getIconCompat(id: Int): IconCompat {
|
||||
return if (Build.VERSION.SDK_INT >= 26)
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
IconCompat.createWithAdaptiveBitmap(getBitmap(id))
|
||||
else
|
||||
IconCompat.createWithBitmap(getBitmap(id))
|
||||
}
|
||||
|
||||
@RequiresApi(api = 23)
|
||||
private fun Context.getIcon(id: Int) = getIconCompat(id).toIcon(this)
|
||||
|
||||
@RequiresApi(api = 25)
|
||||
|
@@ -1,9 +1,10 @@
|
||||
package com.topjohnwu.magisk.view
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.DiffRvItem
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.RvItem
|
||||
|
||||
sealed class TappableHeadlineItem : DiffRvItem<TappableHeadlineItem>() {
|
||||
sealed class TappableHeadlineItem : RvItem(), DiffItem<TappableHeadlineItem> {
|
||||
|
||||
abstract val title: Int
|
||||
abstract val icon: Int
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user