mirror of https://github.com/topjohnwu/Magisk
Compare commits
1159 Commits
Author | SHA1 | Date |
---|---|---|
LoveSy | c6f0762510 | |
LoveSy | 941a363c5a | |
Arbri çoçka | 2afcdc64a0 | |
VD $ VD171 @ Priv8 | 3c66c4bbc5 | |
VD $ VD171 @ Priv8 | 9f5cd5e1cc | |
kubalav | a35f2bb73b | |
topjohnwu | 6cf00130f4 | |
topjohnwu | 6c27ba6b88 | |
vvb2060 | dd3b9980e7 | |
vvb2060 | 02e189a029 | |
topjohnwu | 72b8d12ee4 | |
topjohnwu | eed03080c1 | |
LoveSy | 090cb4b0f9 | |
topjohnwu | 6f2c76b898 | |
topjohnwu | f61827cbec | |
topjohnwu | 3f2264f2c7 | |
topjohnwu | c1cadf4bdc | |
Rodrigo Martínez | 0e56991369 | |
LoveSy | 4dc1c59040 | |
topjohnwu | 33b7b8b297 | |
topjohnwu | e6af5ed460 | |
topjohnwu | b678afa4b6 | |
WINZORT | 4bac2df4e7 | |
igor | 50416eee09 | |
igor | 73cf501d33 | |
Hen_Ry | d2b7907bed | |
topjohnwu | 99d5dd5ea8 | |
cloudchamb3r | 5fdb841fa8 | |
topjohnwu | 7c88484d64 | |
topjohnwu | b22b6a4204 | |
topjohnwu | 2a3d34c812 | |
topjohnwu | c50ee722a1 | |
topjohnwu | ffc1e38e48 | |
topjohnwu | 6219d5fcbf | |
topjohnwu | 2e4440b702 | |
topjohnwu | 0d9ec0931b | |
vvb2060 | 60e8415369 | |
LoveSy | 652a26d5d9 | |
topjohnwu | f57839379a | |
LoveSy | 36bd00a046 | |
topjohnwu | fb5ee86615 | |
topjohnwu | 30bf5c8448 | |
topjohnwu | 2051836a73 | |
topjohnwu | 2cb0af1ff3 | |
topjohnwu | a1b6568226 | |
topjohnwu | 1eddbfd72c | |
topjohnwu | 21ed095601 | |
Js0n | 000a2e4d59 | |
Js0n | 7abe635de9 | |
topjohnwu | 9a008c17ba | |
topjohnwu | 08dbf728a4 | |
topjohnwu | 4670f762d3 | |
topjohnwu | efa49567fa | |
topjohnwu | 0ffc4527a7 | |
topjohnwu | dd9d43be96 | |
topjohnwu | 865fca71a5 | |
topjohnwu | 6b4baa3bcd | |
topjohnwu | a9ee2d7d18 | |
topjohnwu | d654b9cb97 | |
LoveSy | 4d2921e742 | |
vvb2060 | ecc74d45d1 | |
vvb2060 | 5de597f079 | |
LoveSy | 156b0e67ca | |
vvb2060 | 10069215f4 | |
LoveSy | 92b305a389 | |
topjohnwu | d20b30c771 | |
topjohnwu | 83209b21ff | |
topjohnwu | 81658d45f7 | |
topjohnwu | c951b208a1 | |
topjohnwu | 050a073771 | |
topjohnwu | 21d374214f | |
LoveSy | 19ea25a9d0 | |
topjohnwu | dbf6e40dfe | |
topjohnwu | d56f4fbc90 | |
topjohnwu | 73c3d741a7 | |
pndwal | 2b5fc75127 | |
osm0sis | 991802ab82 | |
WindowsFan9600 | 7f6b5305ba | |
canyie | 825c6c4316 | |
canyie | f00408c793 | |
topjohnwu | a6ff3672af | |
LoveSy | 2290ddeb89 | |
topjohnwu | 74af79ad03 | |
LoveSy | b6c24a3a8a | |
LoveSy | a8c2ae223a | |
topjohnwu | 953d44302c | |
topjohnwu | 24e46a5971 | |
topjohnwu | b1297c4192 | |
topjohnwu | 9ae328fd84 | |
topjohnwu | 625a1d6f44 | |
topjohnwu | 987e5f5413 | |
topjohnwu | 715284b70d | |
LoveSy | 62fc7868ac | |
topjohnwu | 1a70796339 | |
topjohnwu | af6965eefa | |
topjohnwu | 8f7d2e38f7 | |
topjohnwu | be433fa667 | |
topjohnwu | 0ccd6e7381 | |
topjohnwu | 907bbbda41 | |
topjohnwu | 4393bc077d | |
topjohnwu | 365b373480 | |
topjohnwu | 47e6dd286d | |
topjohnwu | 0dbaf52566 | |
topjohnwu | 66f49dfab5 | |
topjohnwu | f8967e9274 | |
topjohnwu | a4f008fde5 | |
topjohnwu | e9980c778b | |
topjohnwu | 06b6fb0c33 | |
topjohnwu | 38cb3d4105 | |
topjohnwu | db99caf258 | |
topjohnwu | 39dbffadfe | |
topjohnwu | b7505c3c9c | |
topjohnwu | 3185e5a7ca | |
topjohnwu | e0cbe28711 | |
topjohnwu | 66cee19cea | |
topjohnwu | 2ec29ade79 | |
topjohnwu | c865d4e187 | |
topjohnwu | a42a0a53ce | |
topjohnwu | 6d79de7d71 | |
topjohnwu | 7e9abe6e90 | |
残页 | 4d5510be4f | |
topjohnwu | b04e1394c0 | |
topjohnwu | 2aa923191e | |
topjohnwu | 4bf1c74164 | |
topjohnwu | 472c7878b2 | |
topjohnwu | 38ad871e33 | |
topjohnwu | c5d34670c4 | |
topjohnwu | 154121f3dd | |
topjohnwu | 3d91a561fe | |
topjohnwu | 2c6adbc69b | |
topjohnwu | 5280982363 | |
topjohnwu | 18c45ae289 | |
LoveSy | 41fbd2a7be | |
LoveSy | 5e45884af4 | |
topjohnwu | d78ee171bc | |
LoveSy | 356ee1febd | |
LoveSy | cc044ccc4c | |
LoveSy | 9c638cc463 | |
topjohnwu | df786eb2b6 | |
topjohnwu | 8e7186eebb | |
topjohnwu | 74b7b84561 | |
topjohnwu | 308c9999fa | |
topjohnwu | 930bb8687f | |
topjohnwu | f2c4288d2d | |
topjohnwu | b44141ae39 | |
kam821 | 86e0020964 | |
残页 | 94d3daeadf | |
LoveSy | 79334b7702 | |
LoveSy | df66458db6 | |
LoveSy | 97705704e2 | |
topjohnwu | 1206179580 | |
topjohnwu | a0b8aa4da6 | |
topjohnwu | 65207f96c8 | |
Abhishek Girish | 062e498bdd | |
topjohnwu | 1057cb3e3c | |
topjohnwu | 2dd23b2518 | |
RafaeloxMC | 8cab12998c | |
topjohnwu | 48b1c26dc8 | |
topjohnwu | f1e0bc3e4a | |
topjohnwu | 38527cd58f | |
LoveSy | e94d65b4b2 | |
LoveSy | 27ece3c7df | |
LoveSy | 06687abffc | |
vvb2060 | deedb462a0 | |
igor | c48962bdf7 | |
Wang Han | 1ef3f6e13b | |
topjohnwu | 83a34a9004 | |
topjohnwu | e30bda6c8d | |
vvb2060 | 00e9d76a5a | |
LoveSy | 6cda6c2fae | |
VD $ VD171 @ Priv8 | 6dfda6dc39 | |
LoveSy | f41994cb52 | |
topjohnwu | a003336497 | |
LoveSy | 401090d6fe | |
LoveSy | 90dcc1cd30 | |
LoveSy | 2ac464b186 | |
LoveSy | 8b7fae278b | |
topjohnwu | d73c2daf6d | |
topjohnwu | ca25935de3 | |
LoveSy | d7750b7220 | |
LoveSy | 98861f0b5a | |
topjohnwu | e35925d520 | |
Kieron Quinn | 685a2d2101 | |
LoveSy | f7e471616d | |
残页 | c013a349af | |
topjohnwu | 61ea59a27b | |
VD $ VD171 @ Priv8 | e55f338367 | |
VD $ VD171 @ Priv8 | 1425cf4105 | |
topjohnwu | b493a985b0 | |
canyie | 1fe9ede940 | |
LoveSy | 1fd49e4987 | |
LoveSy | d49b02b274 | |
LoveSy | d47e70cfaa | |
topjohnwu | 40cb031af5 | |
topjohnwu | 1dcf325547 | |
LoveSy | 4e99997013 | |
LoveSy | 334554697d | |
LoveSy | e77cbd0c15 | |
topjohnwu | 46ba008b9d | |
LoveSy | 58aded31c2 | |
LoveSy | 6f6b0ade06 | |
topjohnwu | d9b67a207b | |
topjohnwu | c7083659aa | |
topjohnwu | a6d1803105 | |
Re*Index. (ot_inc) | 66eef75673 | |
Alessandro Sangiorgi | 367f5f7b44 | |
topjohnwu | 0edcb03c45 | |
canyie | 63eef153de | |
canyie | 68442f38ac | |
topjohnwu | 8d5b9e5329 | |
topjohnwu | 6c0966b795 | |
topjohnwu | 7c2e93d266 | |
topjohnwu | 1ff7b9055f | |
topjohnwu | 49f241b77c | |
topjohnwu | cfb20b0f86 | |
topjohnwu | 6d6f14fcb3 | |
topjohnwu | 977c981265 | |
topjohnwu | ef48abf19d | |
topjohnwu | 65c18f9c09 | |
残页 | ecb31eed40 | |
topjohnwu | a80cadf587 | |
LoveSy | fce1bf2365 | |
LoveSy | cbc6d40b2c | |
LoveSy | 9fbd079560 | |
LoveSy | 42eb928054 | |
topjohnwu | 577483fde1 | |
topjohnwu | aa6c7c15cc | |
topjohnwu | 6c807d35b2 | |
topjohnwu | 8ca8cdae97 | |
topjohnwu | 75e37be6f3 | |
WindowsFan9600 | 4985314ca6 | |
topjohnwu | ac5ceb18c8 | |
topjohnwu | 72b39594d3 | |
topjohnwu | 16ae4aedf1 | |
topjohnwu | 3ba00858e6 | |
topjohnwu | 489100c755 | |
topjohnwu | da766f2a4e | |
topjohnwu | c81d7ff76c | |
topjohnwu | a6e50d3648 | |
topjohnwu | a177846044 | |
topjohnwu | 19a4e11645 | |
topjohnwu | 67cc36268e | |
topjohnwu | 28770b9a32 | |
WindowsFan9600 | 9f92e1bf15 | |
topjohnwu | 23fe5d5a19 | |
LoveSy | 9088b584f6 | |
vvb2060 | beaf636415 | |
vvb2060 | 09bb2fe8dc | |
tzagim | 1d6747d90e | |
南宫雪珊 | efadd94de3 | |
vvb2060 | 8c0b4e444a | |
Rom | 32c7106e40 | |
topjohnwu | d2f2a9e4c8 | |
topjohnwu | 985454afd4 | |
topjohnwu | 9e1322de25 | |
topjohnwu | 4e4ec73d94 | |
topjohnwu | bb39a524d0 | |
topjohnwu | 196d9af099 | |
topjohnwu | 1eeb2a34a1 | |
Arbri çoçka | cf43c56218 | |
kubalav | e6c1aec443 | |
topjohnwu | 43fd1c4c1b | |
topjohnwu | 022caca979 | |
topjohnwu | 0352ea2cca | |
topjohnwu | e483d6befe | |
vvb2060 | 678c07fff5 | |
topjohnwu | 91c92051f1 | |
topjohnwu | 4b8a0388e7 | |
topjohnwu | 66788dc58c | |
topjohnwu | dd8c28b1cb | |
残页 | 32c5153e8e | |
topjohnwu | 36de62873a | |
topjohnwu | 51e37880c6 | |
topjohnwu | 4b83c1e76c | |
topjohnwu | b0b04690d5 | |
topjohnwu | 6d1e8d86cb | |
topjohnwu | eda8c70a80 | |
topjohnwu | 587b6cfd41 | |
topjohnwu | e774408782 | |
canyie | 187f583c95 | |
topjohnwu | f5d3a71478 | |
残页 | d868ff3080 | |
nkh0472 | f80198a669 | |
topjohnwu | 6076b52c48 | |
topjohnwu | 79a1c39b30 | |
topjohnwu | 5c92d39498 | |
topjohnwu | 6e7a995716 | |
topjohnwu | a55d570213 | |
topjohnwu | 5d07d0b964 | |
Wang Han | ec115cd7e3 | |
osm0sis | 9b3896fd3d | |
topjohnwu | a3f5918d25 | |
topjohnwu | b28326198c | |
topjohnwu | 46275b90c2 | |
topjohnwu | 15e13a8d8b | |
topjohnwu | b750c89c87 | |
LoveSy | 8d7c7c3dfb | |
topjohnwu | 8e1a91509c | |
LoveSy | 927cd571f8 | |
LoveSy | 5fbd3e5c65 | |
LoveSy | 877aeb66cb | |
topjohnwu | 8a88d8465a | |
topjohnwu | dda8cc85c9 | |
topjohnwu | 6a59939d9a | |
topjohnwu | 4745e86c1b | |
topjohnwu | 9aa466c773 | |
LoveSy | 0243610c09 | |
topjohnwu | 0a2a590ab7 | |
topjohnwu | 89aee6ffa7 | |
topjohnwu | 4eaf701cb7 | |
topjohnwu | 4fff2aa7d8 | |
topjohnwu | 35b3c8ba5c | |
topjohnwu | 1d7cff7703 | |
LoveSy | 8d81bd0e33 | |
topjohnwu | 7826d7527f | |
topjohnwu | d4e552d08b | |
topjohnwu | 5a16418543 | |
topjohnwu | 7297aba15a | |
Ylarod | bc5d5f9502 | |
vvb2060 | 1761986c1b | |
topjohnwu | 1e034e3e0e | |
topjohnwu | bbf9756bfa | |
topjohnwu | 96e559fb0e | |
topjohnwu | 4c45775131 | |
LoveSy | c072b4254d | |
topjohnwu | 2dbb812126 | |
topjohnwu | be50f17f55 | |
残页 | 6f77f190f2 | |
topjohnwu | 6bdc57cbe4 | |
残页 | de00f1d5a9 | |
残页 | e9b9bf987b | |
topjohnwu | f4b6385f9f | |
topjohnwu | 75d905a56d | |
topjohnwu | b1363ee479 | |
topjohnwu | 51afe43a30 | |
残页 | 189c03c047 | |
topjohnwu | ae9d270a32 | |
topjohnwu | e47e869f6b | |
topjohnwu | c39038a439 | |
topjohnwu | 69174e2c13 | |
Chris Renshaw | 474268a0af | |
topjohnwu | eadb0307fa | |
topjohnwu | 5a5d0d5d72 | |
topjohnwu | a1273bc467 | |
topjohnwu | 0c28a916be | |
BlackMesa123 | 0ba573b789 | |
BlackMesa123 | ec42ee152c | |
I3elphegor | abcb487361 | |
vvb2060 | d12d9e82f1 | |
topjohnwu | 275208e81b | |
topjohnwu | 41226c12b8 | |
topjohnwu | f86c66c99d | |
topjohnwu | 93876b5fd3 | |
topjohnwu | b5b14ce343 | |
topjohnwu | 350d0d600c | |
topjohnwu | f924ffcbf3 | |
VD $ VD171 @ Priv8 | 0f5963f231 | |
Arbri çoçka | 1961ff2c40 | |
vvb2060 | 40003691d6 | |
topjohnwu | 8290358241 | |
kubalav | ee34f775c3 | |
vvb2060 | feb47cd88c | |
vvb2060 | c6efb51f61 | |
Hen_Ry | a5acf33ccd | |
vvb2060 | ab9ee449e4 | |
vvb2060 | 9571b6f9be | |
vvb2060 | 207d7fd3f6 | |
南宫雪珊 | bcdcfa1104 | |
vvb2060 | e0a4230dac | |
topjohnwu | 17ba5cba3e | |
topjohnwu | f2e109ad7d | |
topjohnwu | c83e141a1c | |
topjohnwu | 6089cc36de | |
topjohnwu | 9638dc0a66 | |
Andrew Gunnerson | b191a14a23 | |
topjohnwu | cf1bc82537 | |
残页 | 6141bb5bb3 | |
topjohnwu | 4d2b62da0d | |
topjohnwu | 39383229d1 | |
topjohnwu | 08bfbb154a | |
残页 | d390ca2fdf | |
topjohnwu | 7ad77a14ae | |
topjohnwu | f33343b4e6 | |
topjohnwu | af65d07456 | |
topjohnwu | 16d728f379 | |
topjohnwu | c97ab690b6 | |
topjohnwu | 4caed73fe0 | |
BlackMesa123 | 4856da1584 | |
LoveSy | 0a07018fec | |
LoveSy | 64c82e1f2c | |
topjohnwu | e8e8afa6c2 | |
LoveSy | af2207433d | |
LoveSy | 75ba62d588 | |
LoveSy | 606d97ae4d | |
topjohnwu | d778b0b0a7 | |
topjohnwu | 5ee6daf126 | |
Fs00 | 43b9a09c9b | |
Fs00 | 8475a2bb94 | |
Rom | d8692de2f4 | |
LoveSy | 33a9abc946 | |
topjohnwu | ee943afbc9 | |
LoveSy | 1f7c3e9f14 | |
topjohnwu | 46770db18b | |
vvb2060 | 92f980601c | |
vvb2060 | d0b8c16651 | |
LoveSy | a470ee6f93 | |
vvb2060 | ff1c56683d | |
topjohnwu | 4ee4cbada6 | |
topjohnwu | dbc2236dd2 | |
topjohnwu | a8c4a33e91 | |
topjohnwu | 279f955a84 | |
topjohnwu | fbd1dbb20c | |
topjohnwu | 6c09fc2e64 | |
LoveSy | f3304b482c | |
LoveSy | 0a85ef61c3 | |
topjohnwu | dc26ad7125 | |
LoveSy | 24b1c607f3 | |
topjohnwu | 732a161b67 | |
topjohnwu | 9c7cf340a1 | |
topjohnwu | 399b9e5eba | |
topjohnwu | 5805573625 | |
topjohnwu | a6b1149b9f | |
LoveSy | 51e985ae7f | |
vvb2060 | 9929b25339 | |
topjohnwu | 2359cfc480 | |
topjohnwu | 81493475f9 | |
Arbri çoçka | 0493829231 | |
VD $ VD171 @ Priv8 | e2d1952ad9 | |
LoveSy | 7450965458 | |
Vladimír Kubala | f45384685b | |
topjohnwu | 8abcccc262 | |
LoveSy | a9c89cbbbb | |
topjohnwu | d2eaa6e6c1 | |
LoveSy | 53257b6ea1 | |
LoveSy | c874391be4 | |
LoveSy | 7e8e013832 | |
topjohnwu | 037f46f7f0 | |
topjohnwu | d3e1c496ca | |
topjohnwu | d7d0a44693 | |
topjohnwu | 9d6f6764cb | |
topjohnwu | cb3ab63815 | |
topjohnwu | caae932117 | |
LoveSy | e9cf27eb5a | |
LoveSy | 6ee6685f4c | |
LoveSy | d15017b777 | |
LoveSy | a9387e63e1 | |
topjohnwu | 23c1f0111b | |
LoveSy | 866386e21f | |
LoveSy | bf10496fa9 | |
LoveSy | 607e6547a7 | |
topjohnwu | 6b21091fe2 | |
topjohnwu | e58f98e844 | |
LoveSy | b8cb9cd84d | |
LoveSy | c1038ac6f9 | |
LoveSy | c556dd0aac | |
LoveSy | d2fbcd07b7 | |
LoveSy | bf6359abaa | |
topjohnwu | d1621845b8 | |
topjohnwu | f33f1d25d0 | |
topjohnwu | 40f25f4d56 | |
topjohnwu | e13775ec2c | |
topjohnwu | ee4dad7a13 | |
topjohnwu | 5e2ef1b7f4 | |
topjohnwu | f8c38eab74 | |
topjohnwu | 305e8b3d14 | |
topjohnwu | 2a654e5d7f | |
topjohnwu | 57afae3425 | |
topjohnwu | feb44f875e | |
topjohnwu | 7eebe62bb6 | |
topjohnwu | 9ea9f01933 | |
topjohnwu | 665c6bdc4b | |
topjohnwu | c79bc83275 | |
topjohnwu | c30fbdf145 | |
topjohnwu | f12951bd1d | |
nikk gitanes | 52f2e8c4a0 | |
nikk gitanes | 1b2af1ed6d | |
nikk gitanes | 0f9b2a7df8 | |
topjohnwu | f2846694e1 | |
topjohnwu | e668dbf6f7 | |
topjohnwu | d77a368176 | |
topjohnwu | ad0da08610 | |
topjohnwu | 0c52385ad4 | |
topjohnwu | 5b8b48ccc1 | |
topjohnwu | 659b9c6fee | |
LoveSy | ec31cab5a7 | |
vvb2060 | dd93556ad8 | |
topjohnwu | 533aeadd38 | |
topjohnwu | 18d0cedbe2 | |
topjohnwu | 5a94ef9106 | |
topjohnwu | 8e8f01f8b5 | |
topjohnwu | 7087badf87 | |
topjohnwu | 47d2d4e3a5 | |
topjohnwu | 6c47d8f556 | |
topjohnwu | 8c9d0314fb | |
topjohnwu | 69144942e3 | |
topjohnwu | 5627053b74 | |
topjohnwu | 0f666de5e6 | |
LoveSy | eddc862fa3 | |
LoveSy | 4327682120 | |
LoveSy | af5bdee78f | |
LoveSy | 0e36e86dbf | |
LoveSy | f95478f1f1 | |
topjohnwu | 9fe8741a02 | |
topjohnwu | a5768e02ea | |
topjohnwu | f5aaff2b1e | |
topjohnwu | 655f778171 | |
topjohnwu | 2e77a426b2 | |
topjohnwu | 2bcf2e76f1 | |
topjohnwu | 57bd450798 | |
topjohnwu | 582cad1b8d | |
topjohnwu | 6ca2a3d841 | |
topjohnwu | 91773c3311 | |
topjohnwu | dc61033b2c | |
topjohnwu | f8d62a4b6c | |
topjohnwu | 1d2145b1b7 | |
topjohnwu | 1f7f84b74a | |
topjohnwu | cd7a335d0f | |
topjohnwu | 17569005a4 | |
topjohnwu | f36b21bae5 | |
topjohnwu | fe1ca52f6d | |
topjohnwu | 1be647a279 | |
topjohnwu | 2ea1a47bec | |
Ernest | 2d799dae0d | |
Ernest | c6408babac | |
topjohnwu | a8c1ed8795 | |
topjohnwu | e3cb5f8ddd | |
topjohnwu | e160e211dd | |
topjohnwu | 22d05ca399 | |
vvb2060 | bd2651057d | |
topjohnwu | 1610092ec4 | |
LoveSy | b9e6937996 | |
topjohnwu | a207f03952 | |
topjohnwu | 851153dd7c | |
topjohnwu | 583ffc8177 | |
topjohnwu | 7518092ad2 | |
topjohnwu | 8c2ad3883a | |
topjohnwu | d364554425 | |
vvb2060 | 726ffdcd98 | |
vvb2060 | f9d22cf8ee | |
vvb2060 | ee50da566f | |
vvb2060 | 9f7d410959 | |
vvb2060 | bc94ea4334 | |
topjohnwu | c0c9204848 | |
topjohnwu | c0d1bf63bc | |
StoyanDimitrov | bbda0cdffe | |
topjohnwu | 7b5ff99cd1 | |
topjohnwu | 21ddb26db8 | |
LoveSy | 7bf2e3875f | |
topjohnwu | b136aba1e2 | |
topjohnwu | 0d84f80b3c | |
topjohnwu | af45aeb771 | |
topjohnwu | 1c5a435e1f | |
Soo-Hwan Na | 0ea1257dcd | |
Mohamadreza Nakhleh | 4c92677b5a | |
fadlyas07 | 979260bd62 | |
topjohnwu | f7de649a36 | |
topjohnwu | 0cf0d2b821 | |
vvb2060 | 3733c9a091 | |
vvb2060 | e9f32e4f68 | |
vvb2060 | 68c2817d40 | |
naxitoo | 83d837d868 | |
I3elphegor | 093eb15ee1 | |
VD $ VD171 @ Priv8 | c6412c1b1b | |
serkanege | 1151393d74 | |
topjohnwu | 468f3efb13 | |
LoveSy | d6b19b9d4c | |
Ilya Kushnir | 709f25f600 | |
topjohnwu | 4b16e4b026 | |
topjohnwu | cdfbc02922 | |
topjohnwu | d0c9384233 | |
topjohnwu | 2488668b06 | |
LoveSy | 52a98cbd51 | |
serkanege | 1840c4c486 | |
serkanege | 34080f3958 | |
topjohnwu | e9b76b6aa5 | |
Jakub K | b7799b53d9 | |
Lishoo | 1e206515c7 | |
sn-o-w | 6bb313184d | |
l3ng | 2763992434 | |
osm0sis | 18fe0e6442 | |
zjw | a70c73bffd | |
topjohnwu | b4ae3493a6 | |
残页 | 1a16004b20 | |
topjohnwu | 56707b8119 | |
LoveSy | c3f9533ddc | |
Rom | 3b3abd63cc | |
Hen_Ry | 411d3ed4e9 | |
LoveSy | f29cc26103 | |
Ilya Kushnir | 1cd595a598 | |
topjohnwu | 22e023b58d | |
topjohnwu | 7be958e35d | |
topjohnwu | 69b66ef637 | |
topjohnwu | daf8653c38 | |
topjohnwu | e2545e57cf | |
topjohnwu | 7cb0909c70 | |
topjohnwu | cc5ff36165 | |
topjohnwu | 18b1ef6c29 | |
LoveSy | 7fe012347a | |
vvb2060 | 5c165c9bb0 | |
topjohnwu | 6c3519923d | |
topjohnwu | 9ea859810d | |
LoveSy | 8dae7b5451 | |
vvb2060 | f827755aaf | |
topjohnwu | 637a8af234 | |
LoveSy | b0fc580860 | |
vvb2060 | 9279f30e89 | |
LoveSy | b505819ca2 | |
topjohnwu | 39d1d23909 | |
vvb2060 | 69529ac59c | |
vvb2060 | a18a440236 | |
LoveSy | aa7846c1c0 | |
topjohnwu | 24ba4ab95b | |
topjohnwu | 762b70ba9d | |
topjohnwu | 41b77e4f25 | |
topjohnwu | 2087e47300 | |
vvb2060 | 46ce765860 | |
topjohnwu | 5117dc1a31 | |
Arbri çoçka | 620fd7d124 | |
kubalav | 3e991dc003 | |
LoveSy | 15cab86152 | |
LoveSy | aa785b5845 | |
LoveSy | 97731a519a | |
残页 | b696dae808 | |
topjohnwu | 732a8260c2 | |
LoveSy | 4ff60ef9a9 | |
topjohnwu | 23b1b69110 | |
LoveSy | 3a4fe53f27 | |
LoveSy | e48afff5e8 | |
topjohnwu | 3f4f4598e8 | |
LoveSy | 3921e9cb1b | |
topjohnwu | 0b987dd0b0 | |
Ilya Kushnir | 1620e15f99 | |
topjohnwu | b089511e91 | |
Arbri çoçka | 958788c1aa | |
LoveSy | b5a8a27296 | |
kubalav | 98123775ad | |
Thonsi | c7133974be | |
LoveSy | 04324a7ebe | |
vvb2060 | f54daa3469 | |
LoveSy | 07c22ccd39 | |
LoveSy | e893c13cf1 | |
LoveSy | dba5020e4f | |
LoveSy | 87e036a190 | |
LoveSy | 3dd94672b0 | |
LoveSy | 004b193f69 | |
topjohnwu | 4417997749 | |
LoveSy | 2eef542054 | |
LoveSy | a07d4080b6 | |
LoveSy | b9d0a3b3d4 | |
topjohnwu | 76405bd984 | |
topjohnwu | 4e2b88b3d0 | |
LoveSy | 7048aa1014 | |
LoveSy | 1c2fcd14b5 | |
vvb2060 | 84e1bd7bc3 | |
vvb2060 | 362eea741f | |
LoveSy | 4de93cfd4b | |
LoveSy | 03cee0b8d4 | |
LoveSy | 54ecc001f4 | |
LoveSy | 5c325d9466 | |
topjohnwu | 0e851cdcf8 | |
topjohnwu | af054e4e31 | |
Chris Renshaw | 33fb4653f0 | |
LoveSy | d9f0aed571 | |
LoveSy | 98813c24fb | |
topjohnwu | 3cc81bb3fd | |
topjohnwu | 366dd52419 | |
topjohnwu | fe6b658c02 | |
LoveSy | 3cf66d1c57 | |
topjohnwu | 382568bd3c | |
LoveSy | d130aa02a1 | |
LoveSy | 1a1646795f | |
LoveSy | d52ea1b068 | |
LoveSy | e14f7b6908 | |
南宫雪珊 | 4709a32641 | |
topjohnwu | 71b7f52663 | |
LoveSy | 981ccabbef | |
vvb2060 | 9e07eb592c | |
LoveSy | 9555380818 | |
topjohnwu | f80d5d858e | |
topjohnwu | a1ce6f5f12 | |
LoveSy | 1aade8f8a8 | |
LoveSy | b9213b7043 | |
LoveSy | 4af72324f4 | |
LoveSy | b6ea5b8984 | |
topjohnwu | c279e08c88 | |
topjohnwu | 2717feac21 | |
topjohnwu | 8adf27859d | |
LoveSy | 307cf87215 | |
Takeda-senpai | ca31412c05 | |
LoveSy | f59fbd5dca | |
topjohnwu | 2285f5e888 | |
LoveSy | da36e5bcd5 | |
Prithvi | 4ed9f57fdc | |
Daki Carnhof | ea7be6162f | |
南宫雪珊 | 3726eb6032 | |
vvb2060 | 6e918ffd68 | |
vvb2060 | 4772868d6a | |
vvb2060 | 78df677a42 | |
vvb2060 | 85a4b249b3 | |
vvb2060 | d06e9a0b51 | |
vvb2060 | 5eb774a2ad | |
topjohnwu | cbbbbab483 | |
LoveSy | e5641d5bdb | |
topjohnwu | a721206c6f | |
LoveSy | c7a27481f9 | |
LoveSy | 594c304634 | |
topjohnwu | d0ec387c28 | |
vvb2060 | 7dbfba76bf | |
vvb2060 | 2a4aa95a6f | |
vvb2060 | 5520f0fbf7 | |
LoveSy | a1a87c9956 | |
vvb2060 | 2c53356bfd | |
topjohnwu | 85d9756f62 | |
LoveSy | 79586ece4c | |
AndroPlus | 6851d11a8e | |
LoveSy | 996a857096 | |
LoveSy | d7158131e4 | |
topjohnwu | 3d3082bc82 | |
topjohnwu | 744ebca206 | |
topjohnwu | 92077ebe53 | |
LoveSy | 78ca682bc5 | |
LoveSy | af01a36296 | |
LoveSy | 97ed1b16d0 | |
LoveSy | 508a001753 | |
vvb2060 | c1909d520b | |
topjohnwu | 9b1e173373 | |
LoveSy | 4ba365565f | |
残页 | ae34659b26 | |
LoveSy | 79a85f5937 | |
LoveSy | b249832571 | |
LoveSy | 577b5912af | |
LoveSy | 9e8c68af12 | |
shìwēi nguyen | 03418ddcbf | |
LoveSy | 220a1c84ce | |
南宫雪珊 | 9a4458ffac | |
vvb2060 | 7a9e6d2ad2 | |
LoveSy | 9656cf2f86 | |
BlackMesa123 | 584bad5314 | |
topjohnwu | 459088024f | |
Chris Renshaw | d740bbe058 | |
canyie | 6ecc04a4df | |
canyie | 15a7e9af57 | |
LoveSy | 0329f00129 | |
topjohnwu | cd8a2edefb | |
LoveSy | 4318ab5cd2 | |
topjohnwu | 3517e6d752 | |
LoveSy | 67845f9c21 | |
Kian-Meng Ang | f562710438 | |
vvb2060 | e836909c50 | |
vvb2060 | 7769ba5f54 | |
topjohnwu | 7fe9db90a1 | |
topjohnwu | 8f7d6dfb77 | |
canyie | 2839978cc1 | |
canyie | e73f87b758 | |
canyie | bd0409fd15 | |
canyie | babdfe80cb | |
topjohnwu | 636223b289 | |
LoveSy | aa0a2f77cf | |
topjohnwu | e38f35eab2 | |
canyie | cb39514705 | |
topjohnwu | 78a444d601 | |
LoveSy | 37b81ad1f6 | |
vvb2060 | 7871c2f595 | |
topjohnwu | 57d83635c6 | |
topjohnwu | 76fbf4634a | |
topjohnwu | 7ce4bd3330 | |
vvb2060 | ad0e6511e1 | |
vvb2060 | a4a734458b | |
Brian Kepha | f989756b93 | |
LoveSy | 5763a3d908 | |
topjohnwu | 1b745ae1a0 | |
topjohnwu | b6d50bea2c | |
topjohnwu | 831a398bf1 | |
topjohnwu | a848783b97 | |
LoveSy | 4d876f0145 | |
LoveSy | bdfedea4e0 | |
LoveSy | ea0e3a09ef | |
topjohnwu | dadae20960 | |
LoveSy | 4ed34cd648 | |
osm0sis | 0d38c94c9c | |
vvb2060 | 2a2a452bd4 | |
vvb2060 | 13c2695e98 | |
fadlyas07 | 3ff60ed49f | |
VD $ VD171 @ Priv8 | bbb1786ec3 | |
Davy Defaud | 4bfd2dac54 | |
ysard | 857c12372a | |
残页 | 33f5154269 | |
topjohnwu | ed37ddd570 | |
LoveSy | cd5384f13e | |
LoveSy | 11b2ddbad8 | |
topjohnwu | cf9957ce4d | |
topjohnwu | 44643ad7b3 | |
topjohnwu | 1e53a5555e | |
topjohnwu | 616adc22e1 | |
akhilkedia | 916e373edb | |
Hen_Ry | 021ae15395 | |
vvb2060 | 52cf72002a | |
topjohnwu | 68874bf571 | |
残页 | a468fd946d | |
topjohnwu | e327565434 | |
topjohnwu | c3b4678f6e | |
vvb2060 | 978216eade | |
残页 | 44cfe94e4d | |
Nitrovenom | f9e82c9e8a | |
theunknownKiran | 25b4b107d3 | |
theunknownKiran | db651fa9ec | |
LoveSy | 23ad611566 | |
topjohnwu | 095d821240 | |
topjohnwu | e23f23a8b7 | |
topjohnwu | 48f829b76e | |
topjohnwu | 0b82fe197c | |
topjohnwu | af99c1b843 | |
topjohnwu | c6646efe68 | |
Nitrovenom | 66a7ef5615 | |
canyie | 9474750bdf | |
LoveSy | e86db0bd61 | |
topjohnwu | a29fc11798 | |
topjohnwu | a66a3b7438 | |
topjohnwu | 44029875a6 | |
topjohnwu | ccf21b0992 | |
topjohnwu | 4e14dab60a | |
topjohnwu | 6e299018a4 | |
topjohnwu | 555a54ec53 | |
topjohnwu | 1565bf5442 | |
topjohnwu | 14b830027b | |
topjohnwu | 38325e708e | |
topjohnwu | 646260ad6d | |
topjohnwu | d1d26f4481 | |
topjohnwu | 357d913f18 | |
topjohnwu | 71b0c8b42b | |
topjohnwu | cdc66c1ac8 | |
topjohnwu | e9af773901 | |
Rom | eadf6e8b96 | |
topjohnwu | 87bec70d9f | |
Ilya Kushnir | 3668b28f62 | |
Arbri çoçka | 933e4bd163 | |
vvb2060 | e3ab9e9a1e | |
VD $ VD171 @ Priv8 | 58ad2c1416 | |
kubalav | c5291ad33b | |
vvb2060 | 77d8445bfd | |
topjohnwu | f8395a7dc6 | |
topjohnwu | 727c70005e | |
topjohnwu | 38ab6858f0 | |
topjohnwu | a54114f149 | |
topjohnwu | 7a4a5c8992 | |
topjohnwu | 928a16d8cc | |
topjohnwu | 3f7f6e619a | |
vvb2060 | c2f96975ce | |
vvb2060 | 8bd4760b00 | |
vvb2060 | 4f4aeb893d | |
canyie | fed4f1b50f | |
vvb2060 | e11087cd1a | |
南宫雪珊 | e6eb51551c | |
topjohnwu | c5c608f0d3 | |
topjohnwu | 4737c5117a | |
topjohnwu | 9806b38d8e | |
topjohnwu | 6bfe34e5a8 | |
topjohnwu | 34dd9eb7d6 | |
topjohnwu | 2d8beabbd4 | |
topjohnwu | 4d9b7e7114 | |
topjohnwu | 40aab13601 | |
topjohnwu | 4c0f72f68f | |
vvb2060 | dd565a11ea | |
残页 | 1735a713cb | |
残页 | 52ba6d11bc | |
topjohnwu | 7357a35f8d | |
Acetylcholine | aeb7fd7cb3 | |
topjohnwu | 1b4a6850b8 | |
Cristian Silaghi | 07b45f39df | |
canyie | 1d0b873950 | |
topjohnwu | d449f49d73 | |
canyie | e8787b5cfd | |
topjohnwu | d17ed2b979 | |
topjohnwu | b496923cbb | |
topjohnwu | 759d196aad | |
topjohnwu | a7ab8216ce | |
topjohnwu | b9e89a1a2d | |
vvb2060 | c7c9fb9576 | |
vvb2060 | 8b095de04d | |
vvb2060 | 468325b51a | |
gidano | e5058bfb8b | |
vvb2060 | d4b9ef736d | |
vvb2060 | 00d3cb0908 | |
vvb2060 | d35072d4e6 | |
canyie | 1a964e78dd | |
topjohnwu | 4264ae49c0 | |
topjohnwu | f08712cd0a | |
LoveSy | 3906fe75dc | |
topjohnwu | 2497e548c9 | |
topjohnwu | e4635684e9 | |
topjohnwu | 9b61bdfc9a | |
topjohnwu | 6066b5cf86 | |
topjohnwu | 5cdf95a4d0 | |
topjohnwu | 910a36fdc1 | |
topjohnwu | 8331206acb | |
canyie | 8423dc8d63 | |
Yann | 6077c989a7 | |
topjohnwu | c97d1044fa | |
Hen_Ry | f42c089b26 | |
Andrew Gunnerson | 1f8c063dc6 | |
Hen_Ry | 4874520d65 | |
Nguyen Hoang The Vi | 5e53639969 | |
Grammatopoulos Apostolos | 83ab0ca6cd | |
topjohnwu | 70fd03d5fc | |
topjohnwu | 2e52875b50 | |
topjohnwu | fd9b990ad7 | |
LONE DEVIL | 69978a9442 | |
残页 | d155da52ce | |
Weslley Almeida | 9c5b131913 | |
Syuugo | 9d740cec1a | |
vvb2060 | c2978eb9c3 | |
vvb2060 | 38abad1e44 | |
topjohnwu | b4863eb51b | |
LoveSy | 3817167ba1 | |
topjohnwu | d1a35dd2ba | |
topjohnwu | 26116ac414 | |
topjohnwu | 0b26882fce | |
Nicolás | a2495fb5fb | |
vvb2060 | 0beb3bf16a | |
vvb2060 | b68658e974 | |
LoveSy | 3ae7344747 | |
topjohnwu | 4eb71830b3 | |
topjohnwu | 9183a0a6ea | |
topjohnwu | bb64ba0ef6 | |
topjohnwu | d89a568897 | |
topjohnwu | 9fd1f41e8b | |
孟武.尼德霍格.龍 | c1ab348673 | |
canyie | 00247c7901 | |
topjohnwu | 3c75f474c6 | |
topjohnwu | db1f5b0397 | |
fadlyas07 | db277c3e55 | |
vvb2060 | b9c93c66f6 | |
vvb2060 | a250e2b56c | |
残页 | cd96454886 | |
topjohnwu | 741b679306 | |
topjohnwu | 90013e486d | |
LoveSy | 4e2ecdb920 | |
topjohnwu | 6e5df1f06b | |
topjohnwu | 9469e79e3c | |
topjohnwu | db78c20161 | |
topjohnwu | 1699da1754 | |
canyie | 754e690274 | |
topjohnwu | 6f74ed6ceb | |
canyie | 71205bc530 | |
Chris Renshaw | 10e236abdf | |
残页 | 2248af00f3 | |
topjohnwu | 7e61716277 | |
topjohnwu | 50edb8d072 | |
topjohnwu | 515f81944c | |
topjohnwu | 46d4708386 | |
topjohnwu | aabc36f86b | |
nikk gitanes | e0d5d90267 | |
topjohnwu | 482a5b991b | |
CDzungx | 20124fe410 | |
Softastur | f8dcec116a | |
Ilya Kushnir | 343a339aae | |
vvb2060 | 42606efe56 | |
vvb2060 | cae58c8790 | |
topjohnwu | 3a39dd4049 | |
canyie | 89ff3c6572 | |
topjohnwu | 7bf9c74216 | |
topjohnwu | e2f3753551 | |
topjohnwu | cacf873645 | |
NeoHBz | 11e1e7ee36 | |
vvb2060 | 87801b6f23 | |
topjohnwu | 7ce4789e17 | |
topjohnwu | 9dc6d9afce | |
MCPEngu | d6a5354bff | |
Ilya Kushnir | 07af37475b | |
Oliver Cervera | 1b9c273b10 | |
AndroPlus-org | 262c52db56 | |
topjohnwu | eb777296d4 | |
topjohnwu | fc70a384d3 | |
vvb2060 | 34b2f525a3 | |
vvb2060 | 569e9ad937 | |
vvb2060 | c495b3d183 | |
Softastur | 8b16bfbb54 | |
Arbri çoçka | b2f1fd9966 | |
topjohnwu | 317153c53a | |
topjohnwu | fa60daf9b5 | |
canyie | aadb2d825c | |
kubalav | 0e7fe537e3 | |
VD $ VD171 @ Priv8 | 409de3ac44 | |
RV7PR | 759055eaa5 | |
topjohnwu | 9016e6727d | |
残页 | a3381da7ed | |
topjohnwu | 351e094440 | |
残页 | 2106751ea4 | |
vvb2060 | 7ae3cd1c43 | |
topjohnwu | edfd4dcddf | |
topjohnwu | fb89cf1367 | |
Rom | b7b345cf8a | |
残页 | 0be487e47e | |
topjohnwu | 5471147422 | |
topjohnwu | 6305159c5e | |
topjohnwu | 2ed092c9db | |
topjohnwu | 5c6a7ffa6f | |
topjohnwu | 9ab7550970 | |
topjohnwu | 47e7a0a434 | |
RikkaW | 4cc5e9f986 | |
RikkaW | 6a2ae89846 | |
残页 | 3c93539e02 | |
topjohnwu | 05e5ac2ad2 | |
topjohnwu | 10b1782732 | |
topjohnwu | e029994ef8 | |
vvb2060 | 9679874874 | |
topjohnwu | 8186f253e8 | |
topjohnwu | d4fe8632ec | |
vvb2060 | d7776f6597 | |
残页 | 3219d945f5 | |
Takeda-senpai | 8a73a16029 | |
VD $ VD171 @ Priv8 | ce90f9b60d | |
VD $ VD171 @ Priv8 | bdf54d562f | |
Rom | e744cc8ea6 | |
Ilya Kushnir | babcf36495 | |
topjohnwu | e4094c0caa | |
topjohnwu | 2e51fe20a1 | |
vvb2060 | c29636c452 | |
vvb2060 | 22017a5543 | |
topjohnwu | 50e2f33d1c | |
topjohnwu | 5e6eb8dd01 | |
topjohnwu | 18acb97dfe | |
topjohnwu | bf2f823b8c | |
topjohnwu | d0c4226997 | |
topjohnwu | 4ea8bd0229 | |
topjohnwu | ee0d58a9b8 | |
topjohnwu | bf04fa134b | |
Arbri çoçka | 297662cafb | |
kubalav | f464a9b269 | |
vvb2060 | d19fcd5e21 | |
topjohnwu | c0981174a8 | |
vvb2060 | 0b5f973b31 | |
topjohnwu | 4159b3871c | |
南宫雪珊 | 580c993c0b | |
canyie | 0cc29350a0 | |
topjohnwu | 490a784993 | |
topjohnwu | 9c774f96db | |
topjohnwu | 99afe7ac07 | |
topjohnwu | b3f05fd925 | |
topjohnwu | 683cfee88b | |
topjohnwu | 3bcaf0ed5b | |
topjohnwu | edb76503d3 | |
topjohnwu | 484038638f | |
topjohnwu | 8dfb30fefe | |
topjohnwu | 2a252d13b8 | |
topjohnwu | afa364cfc3 | |
topjohnwu | dfa36fb25d | |
topjohnwu | c8492b0c58 | |
topjohnwu | 083ef803fe | |
topjohnwu | 351f0269ae | |
topjohnwu | a29ae15ff7 | |
topjohnwu | 34dded3b25 | |
topjohnwu | 975b1a5e36 | |
vvb2060 | e11508f84d | |
topjohnwu | 0772f6dcaf | |
topjohnwu | d3fe3a711a | |
topjohnwu | 756d8356ca | |
topjohnwu | 42003b4006 | |
topjohnwu | dc65a2b884 | |
topjohnwu | 071ae79fa8 | |
topjohnwu | c11ccbae2d | |
topjohnwu | 6ef86d8d20 | |
topjohnwu | 985249c3d0 | |
topjohnwu | 622e09862a | |
残页 | 7505599ea0 | |
topjohnwu | 575c417403 | |
topjohnwu | 9f7a3db8be | |
topjohnwu | 029422679c | |
vvb2060 | 05d6d2b51b | |
capntrips | 4cff0384f7 | |
vvb2060 | 68db366696 | |
南宫雪珊 | 358538717c | |
topjohnwu | 24603b3cef | |
topjohnwu | 4eb9240806 | |
vvb2060 | 0469f0b5ae | |
vvb2060 | 0b8577d02b | |
Rei Ryuki | 97135879a1 | |
vvb2060 | fef41f68c0 | |
topjohnwu | 0ac19e3a4e | |
topjohnwu | 2793d209a4 | |
topjohnwu | 71e9c044e6 | |
Kazurin Nanako | 42e5f5150a | |
topjohnwu | 90545057e9 | |
vvb2060 | cffd024e9e | |
人工知能 | 8c858592c4 | |
canyie | 4f1a1879e5 | |
JumbomanXDA | e88eed9a8d | |
RikkaW | 9581ae8245 | |
vvb2060 | 4202b7a9dc | |
LoveSy | b4c398542a | |
topjohnwu | 081148b2d7 | |
topjohnwu | a32c4561ed | |
topjohnwu | cc79a96fa3 | |
topjohnwu | ff340ce3d8 | |
topjohnwu | 134508193d | |
topjohnwu | c2b74aa83e | |
topjohnwu | 3358eab991 | |
残页 | a609e0aad4 | |
vvb2060 | f97866a961 | |
vvb2060 | e1987c42c4 | |
canyie | 18566715e1 | |
topjohnwu | 79f0f3230c | |
topjohnwu | 63a89d9f04 | |
南宫雪珊 | f639f39e79 | |
canyie | b4099fc5f9 | |
topjohnwu | ff2513e276 | |
topjohnwu | f24d52436b | |
vvb2060 | 9de6e8846b | |
vvb2060 | 01a1213463 | |
vvb2060 | f0fbd9214a | |
hnliuzesen | c4f37c550f | |
canyie | 448384af06 | |
canyie | 3f840f53a0 | |
Lishoo | d8718d8ac8 | |
vvb2060 | 2fb46a11dc | |
vvb2060 | 9a11412719 | |
topjohnwu | 98874be171 | |
topjohnwu | 704f91545e | |
topjohnwu | efb3239cbd | |
topjohnwu | 7e7ddeb9e2 | |
LoveSy | 9e8218089b | |
VD $ VD171 @ Priv8 | 3f660a3963 | |
VD $ VD171 @ Priv8 | daeb6711b0 | |
CDzungx | 4e1aec28a0 | |
vvb2060 | 5512917ec1 | |
vvb2060 | cd1edc5d56 | |
topjohnwu | 4f52587586 | |
topjohnwu | d7ee4ef5f5 | |
topjohnwu | 31f88e0f05 | |
topjohnwu | 9f1740cc4f | |
topjohnwu | f2c15c7701 | |
topjohnwu | e67d0678f9 | |
topjohnwu | b1faa5eed4 | |
LoveSy | 7f1f0b9048 | |
LoveSy | 183e5f2ecc | |
topjohnwu | 14efe4939a | |
topjohnwu | 3dc7d77ea9 | |
残页 | 0f07bbb3e5 | |
LoveSy | dd5a3416bf | |
LoveSy | 2fb49ad780 | |
topjohnwu | 92f0e53fee | |
topjohnwu | 876132694d | |
topjohnwu | 1257ba41c6 | |
topjohnwu | 2cc71ac7ed | |
topjohnwu | 753808a4ce | |
topjohnwu | 32cd694ad5 | |
topjohnwu | f008420891 | |
topjohnwu | fa8900be65 | |
LoveSy | 69c2f407d6 | |
topjohnwu | ffcd093db1 | |
topjohnwu | 8dbf93750f | |
topjohnwu | e266a81167 | |
topjohnwu | e841aab9e7 | |
topjohnwu | 49f259065d | |
topjohnwu | b10379e700 | |
topjohnwu | 810d27a618 | |
topjohnwu | 9b60c005c7 | |
topjohnwu | cc6ca0bda2 | |
topjohnwu | 4512232637 |
|
@ -1,19 +1,18 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
title: ""
|
||||
labels: ""
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
## READ BEFORE OPENING ISSUES
|
||||
|
||||
All bug reports require you to **USE CANARY BUILDS**. Please include the version name and version code in the bug report.
|
||||
All bug reports require you to **USE DEBUG BUILDS**. Please include the version name and version code in the bug report.
|
||||
|
||||
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT ROOT**.
|
||||
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT MAGISK**.
|
||||
|
||||
If you experience issues during installation, in recovery, upload the recovery logs, or in Magisk, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
|
||||
|
||||
|
@ -31,7 +30,7 @@ Without following the rules above, your issue will be closed without explanation
|
|||
|
||||
-->
|
||||
|
||||
Device:
|
||||
Device:
|
||||
Android version:
|
||||
Magisk version name:
|
||||
Magisk version code:
|
||||
Magisk version name:
|
||||
Magisk version code:
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
name: Magisk Setup
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Set up sccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
variant: sccache
|
||||
key: ${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}
|
||||
max-size: 10000M
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
!~/.gradle/caches/build-cache-*
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||
restore-keys: ${{ runner.os }}-gradle-
|
||||
|
||||
- name: Cache build cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches/build-cache-*
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}-build-cache-
|
||||
|
||||
- name: Set up NDK
|
||||
run: python build.py -v ndk
|
||||
shell: bash
|
|
@ -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'
|
|
@ -2,90 +2,153 @@ name: Magisk Build
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'native/**'
|
||||
- 'stub/**'
|
||||
- 'buildSrc/**'
|
||||
- 'build.py'
|
||||
- 'gradle.properties'
|
||||
- '.github/workflows/build.yml'
|
||||
- "app/**"
|
||||
- "native/**"
|
||||
- "stub/**"
|
||||
- "buildSrc/**"
|
||||
- "build.py"
|
||||
- "gradle.properties"
|
||||
- ".github/workflows/build.yml"
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: Build Magisk artifacts
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SCCACHE_DIRECT: false
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
env:
|
||||
NDK_CCACHE: ${{ github.workspace }}/ccache
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: '11'
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Set up ccache
|
||||
run: bash .github/ccache.sh
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
!~/.gradle/caches/build-cache-*
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||
restore-keys: ${{ runner.os }}-gradle-
|
||||
|
||||
- name: Cache build cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/.ccache
|
||||
~/.gradle/caches/build-cache-*
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}-build-cache-
|
||||
|
||||
- name: Set up NDK
|
||||
run: python build.py -v ndk
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build release
|
||||
run: |
|
||||
./ccache -zp
|
||||
python build.py -vr all
|
||||
run: ./build.py -vr all
|
||||
|
||||
- name: Build debug
|
||||
run: |
|
||||
python build.py -v all
|
||||
./ccache -s
|
||||
run: ./build.py -v all
|
||||
|
||||
- name: Stop gradle daemon
|
||||
run: ./gradlew --stop
|
||||
|
||||
# Only upload artifacts built on Linux
|
||||
- name: Upload build artifact
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
||||
compression-level: 9
|
||||
|
||||
- name: Upload mapping and native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ github.sha }}-symbols
|
||||
path: app/build/outputs
|
||||
compression-level: 9
|
||||
|
||||
test-build:
|
||||
name: Test building on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
SCCACHE_DIRECT: false
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, macos-14]
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build debug
|
||||
run: python build.py -v all
|
||||
|
||||
- name: Stop gradle daemon
|
||||
run: ./gradlew --stop
|
||||
|
||||
test:
|
||||
name: Test x86_64 on API ${{ matrix.api }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
api: [23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34]
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
||||
|
||||
- name: Enable KVM group perms
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: AVD test
|
||||
run: scripts/avd_test.sh ${{ matrix.api }}
|
||||
|
||||
test-32:
|
||||
name: Test x86 on API ${{ matrix.api }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
api: [23, 24, 25, 26, 27, 28, 29, 30]
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
||||
|
||||
- name: Enable KVM group perms
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: AVD test
|
||||
env:
|
||||
FORCE_32_BIT: 1
|
||||
run: scripts/avd_test.sh ${{ matrix.api }}
|
||||
|
|
|
@ -1,45 +1,42 @@
|
|||
[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
|
||||
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
|
||||
url = https://github.com/nanopb/nanopb.git
|
||||
[submodule "mincrypt"]
|
||||
path = native/jni/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 "xhook"]
|
||||
path = native/jni/external/xhook
|
||||
url = https://github.com/iqiyi/xHook.git
|
||||
[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
|
||||
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 "system_properties"]
|
||||
path = native/src/external/system_properties
|
||||
url = https://github.com/topjohnwu/system_properties.git
|
||||
[submodule "crt0"]
|
||||
path = native/src/external/crt0
|
||||
url = https://github.com/topjohnwu/crt0.git
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
[submodule "zopfli"]
|
||||
path = native/jni/external/zopfli
|
||||
url = https://github.com/google/zopfli.git
|
||||
|
|
31
README.MD
31
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,42 +18,25 @@ Some highlight features:
|
|||
|
||||
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
||||
|
||||
[![](https://img.shields.io/badge/Magisk-v24.1-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v24.1)
|
||||
[![](https://img.shields.io/badge/Magisk%20Beta-v24.2-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v24.2)
|
||||
[![](https://img.shields.io/badge/Magisk-Canary-red)](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||
[![](https://img.shields.io/badge/Magisk-v27.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v27.0)
|
||||
[![](https://img.shields.io/badge/Magisk%20Beta-v27.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v27.0)
|
||||
[![](https://img.shields.io/badge/Magisk-Canary-red)](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
||||
[![](https://img.shields.io/badge/Magisk-Debug-red)](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||
|
||||
## Useful Links
|
||||
|
||||
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
||||
- [Building and Development](https://topjohnwu.github.io/Magisk/build.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
|
||||
|
||||
**Only bug reports from Canary builds will be accepted.**
|
||||
**Only bug reports from Debug builds will be accepted.**
|
||||
|
||||
For installation issues, upload both boot image and install logs.<br>
|
||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||
For Magisk app crashes, record and upload the logcat when the crash occurs.
|
||||
|
||||
## Building and Development
|
||||
|
||||
- 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+ \
|
||||
(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"`
|
||||
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
|
||||
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
|
||||
- Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings)
|
||||
- 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.
|
||||
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
|
||||
- To sign APKs and zips with your own private keys, set signing configs in `config.prop`. For more info, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).
|
||||
|
||||
## Translation Contributions
|
||||
|
||||
Default string resources for the Magisk app and its stub APK are located here:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -19,12 +19,17 @@ kapt {
|
|||
}
|
||||
|
||||
android {
|
||||
namespace = "com.topjohnwu.magisk"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.topjohnwu.magisk"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
versionName = Config.version
|
||||
versionCode = Config.versionCode
|
||||
ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
debugSymbolLevel = "FULL"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -37,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/**"
|
||||
|
@ -50,14 +57,6 @@ android {
|
|||
excludes += "/*.bin"
|
||||
excludes += "/*.json"
|
||||
}
|
||||
jniLibs {
|
||||
keepDebugSymbols += "**/*.so"
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs = listOf("-Xjvm-default=enable")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,58 +70,52 @@ configurations.all {
|
|||
dependencies {
|
||||
implementation(project(":app:shared"))
|
||||
|
||||
implementation("com.github.topjohnwu:jtar:1.0.0")
|
||||
implementation("com.github.topjohnwu:jtar:1.1.0")
|
||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
|
||||
implementation("dev.rikka.rikkax.insets:insets:1.1.1")
|
||||
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
|
||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||
implementation("org.bouncycastle:bcpkix-jdk18on:1.77")
|
||||
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.2")
|
||||
implementation("io.noties.markwon:core:4.6.2")
|
||||
|
||||
val vBAdapt = "4.0.0"
|
||||
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
|
||||
implementation("${bindingAdapter}:${vBAdapt}")
|
||||
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
|
||||
|
||||
val vLibsu = "4.0.2"
|
||||
val vLibsu = "5.2.2"
|
||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
||||
|
||||
val vRetrofit = "2.9.0"
|
||||
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
||||
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
||||
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
||||
|
||||
val vOkHttp = "4.9.3"
|
||||
val vOkHttp = "4.12.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.15.0"
|
||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||
|
||||
val vRoom = "2.4.1"
|
||||
val vRoom = "2.6.1"
|
||||
implementation("androidx.room:room-runtime:${vRoom}")
|
||||
implementation("androidx.room:room-ktx:${vRoom}")
|
||||
kapt("androidx.room:room-compiler:${vRoom}")
|
||||
|
||||
val vNav = "2.5.0-alpha01"
|
||||
val vNav = "2.7.7"
|
||||
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.3")
|
||||
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.2")
|
||||
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
||||
implementation("androidx.transition:transition:1.4.1")
|
||||
implementation("androidx.core:core-ktx:1.7.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-beta01")
|
||||
implementation("com.google.android.material:material:1.5.0")
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
}
|
||||
|
|
|
@ -1,21 +1,3 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/topjohnwu/Library/Android/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Parcelable
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
|
@ -26,12 +8,23 @@
|
|||
public static void check*(...);
|
||||
public static void throw*(...);
|
||||
}
|
||||
-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
|
||||
-keep,allowoptimization public class com.topjohnwu.magisk.signing.SignBoot {
|
||||
public static void main(java.lang.String[]);
|
||||
}
|
||||
|
||||
# Strip Timber verbose and debug logging
|
||||
|
@ -40,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
|
||||
|
|
|
@ -5,11 +5,5 @@ plugins {
|
|||
setupCommon()
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
consumerProguardFiles("proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("io.michaelrocks:paranoid-core:0.3.7")
|
||||
namespace = "com.topjohnwu.shared"
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -1,14 +1,20 @@
|
|||
<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" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<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.RUN_USER_INITIATED_JOBS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE"
|
||||
android:maxSdkVersion="33" />
|
||||
<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) {
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
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;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
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;
|
||||
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;
|
||||
|
@ -22,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 {
|
||||
|
@ -50,12 +55,33 @@ public class StubApk {
|
|||
return new File(getDynDir(info), "update.apk");
|
||||
}
|
||||
|
||||
public static void addAssetPath(AssetManager asset, String path) {
|
||||
try {
|
||||
if (addAssetPath == null)
|
||||
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
||||
addAssetPath.invoke(asset, path);
|
||||
} catch (Exception ignored) {}
|
||||
@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 >= Build.VERSION_CODES.R) {
|
||||
try {
|
||||
res.addLoaders(getResourcesLoader(new File(path)));
|
||||
} catch (IOException ignored) {}
|
||||
} else {
|
||||
AssetManager asset = res.getAssets();
|
||||
try {
|
||||
if (addAssetPath == null)
|
||||
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
||||
addAssetPath.invoke(asset, path);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public static void restartProcess(Activity activity) {
|
||||
|
|
|
@ -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,17 +58,15 @@ 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;
|
||||
}
|
||||
|
||||
public interface Session {
|
||||
// @WorkerThread
|
||||
OutputStream openStream(Context context) throws IOException;
|
||||
// @WorkerThread
|
||||
void install(Context context, File apk) throws IOException;
|
||||
// @WorkerThread @Nullable
|
||||
Intent waitIntent();
|
||||
}
|
||||
|
@ -94,27 +99,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();
|
||||
}
|
||||
|
@ -162,13 +165,5 @@ public final class APKInstall {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void install(Context context, File apk) throws IOException {
|
||||
try (var src = new FileInputStream(apk);
|
||||
var out = openStream(context)) {
|
||||
transfer(src, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
package com.topjohnwu.magisk.utils;
|
||||
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import dalvik.system.DexClassLoader;
|
||||
import dalvik.system.BaseDexClassLoader;
|
||||
|
||||
public class DynamicClassLoader extends DexClassLoader {
|
||||
|
||||
private static final ClassLoader base = Object.class.getClassLoader();
|
||||
public class DynamicClassLoader extends BaseDexClassLoader {
|
||||
|
||||
public DynamicClassLoader(File apk) {
|
||||
super(apk.getPath(), apk.getParent(), null, base);
|
||||
this(apk, getSystemClassLoader());
|
||||
}
|
||||
|
||||
public DynamicClassLoader(File apk, ClassLoader parent) {
|
||||
super(apk.getPath(), apk.getParent(), 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
|
||||
|
@ -28,7 +29,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||
|
||||
try {
|
||||
// Then check boot classpath
|
||||
return base.loadClass(name);
|
||||
return getSystemClassLoader().loadClass(name);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
try {
|
||||
// Next try current dex
|
||||
|
@ -46,7 +47,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||
|
||||
@Override
|
||||
public URL getResource(String name) {
|
||||
URL resource = base.getResource(name);
|
||||
URL resource = getSystemClassLoader().getResource(name);
|
||||
if (resource != null)
|
||||
return resource;
|
||||
resource = findResource(name);
|
||||
|
@ -58,7 +59,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||
|
||||
@Override
|
||||
public Enumeration<URL> getResources(String name) throws IOException {
|
||||
return new CompoundEnumeration<>(base.getResources(name),
|
||||
return new CompoundEnumeration<>(getSystemClassLoader().getResources(name),
|
||||
findResources(name), getParent().getResources(name));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.magisk">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<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"
|
||||
|
@ -29,7 +35,6 @@
|
|||
<activity
|
||||
android:name=".ui.surequest.SuRequestActivity"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:taskAffinity=""
|
||||
tools:ignore="AppLinkUrlError">
|
||||
|
@ -41,7 +46,6 @@
|
|||
|
||||
<receiver
|
||||
android:name=".core.Receiver"
|
||||
android:directBootAware="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||
|
@ -57,8 +61,10 @@
|
|||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".core.download.DownloadService"
|
||||
android:exported="false" />
|
||||
android:name=".core.Service"
|
||||
android:exported="false"
|
||||
android:enabled="@bool/enable_fg_service"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".core.JobService"
|
||||
|
@ -77,11 +83,15 @@
|
|||
android:name="androidx.room.MultiInstanceInvalidationService"
|
||||
tools:node="remove" />
|
||||
|
||||
<!-- We don't need emoji compat -->
|
||||
<!-- We handle initialization ourselves -->
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="remove" />
|
||||
|
||||
<!-- We handle profile installation ourselves -->
|
||||
<receiver
|
||||
android:name="androidx.profileinstaller.ProfileInstallReceiver"
|
||||
tools:node="remove" />
|
||||
|
||||
</application>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
// IRootUtils.aidl
|
||||
package com.topjohnwu.magisk.core.utils;
|
||||
|
||||
// Declare any non-default types here with import statements
|
||||
|
||||
interface IRootUtils {
|
||||
android.app.ActivityManager.RunningAppProcessInfo getAppProcess(int pid);
|
||||
IBinder getFileSystem();
|
||||
}
|
|
@ -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,13 +5,14 @@ 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
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.ktx.startAnimations
|
||||
|
||||
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
||||
|
||||
|
@ -20,11 +21,12 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||
protected abstract val layoutRes: Int
|
||||
|
||||
private val navigation get() = activity?.navigation
|
||||
open val snackbarView: View? get() = null
|
||||
open val snackbarAnchorView: View? get() = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
startObserveEvents()
|
||||
startObserveLiveData()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
|
@ -36,9 +38,17 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||
it.setVariable(BR.viewModel, viewModel)
|
||||
it.lifecycleOwner = viewLifecycleOwner
|
||||
}
|
||||
if (this is MenuProvider) {
|
||||
activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
|
||||
}
|
||||
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
viewModel.onSaveState(outState)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
activity?.supportActionBar?.subtitle = null
|
||||
|
@ -70,7 +80,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) {
|
||||
|
@ -78,7 +91,6 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||
}
|
||||
|
||||
fun NavDirections.navigate() {
|
||||
navigation?.navigate(this)
|
||||
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.JobService
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
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
|
||||
|
||||
abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
||||
|
||||
companion object {
|
||||
private var doPreload = true
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(Theme.selected.themeRes)
|
||||
|
||||
if (isRunningAsStub && doPreload) {
|
||||
// Manually apply splash theme for stub
|
||||
theme.applyStyle(R.style.StubSplashTheme, true)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!isRunningAsStub) {
|
||||
val splashScreen = installSplashScreen()
|
||||
splashScreen.setKeepOnScreenCondition { doPreload }
|
||||
}
|
||||
|
||||
if (doPreload) {
|
||||
Shell.getShell(null) {
|
||||
if (isRunningAsStub && !it.isRoot) {
|
||||
showInvalidStateMessage()
|
||||
return@getShell
|
||||
}
|
||||
preLoad()
|
||||
runOnUiThread {
|
||||
doPreload = false
|
||||
if (isRunningAsStub) {
|
||||
// Re-launch main activity without splash theme
|
||||
relaunch()
|
||||
} else {
|
||||
showMainUI(savedInstanceState)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showMainUI(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun showMainUI(savedInstanceState: Bundle?)
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun showInvalidStateMessage(): Unit = runOnUiThread {
|
||||
MagiskDialog(this).apply {
|
||||
setTitle(R.string.unsupport_nonroot_stub_title)
|
||||
setMessage(R.string.unsupport_nonroot_stub_msg)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = R.string.install
|
||||
onClick {
|
||||
withPermission(REQUEST_INSTALL_PACKAGES) {
|
||||
if (!it) {
|
||||
Utils.toast(R.string.install_unknown_denied, Toast.LENGTH_SHORT)
|
||||
showInvalidStateMessage()
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
HideAPK.restore(this@BaseMainActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun preLoad() {
|
||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
|
||||
|
||||
Config.load(prevPkg)
|
||||
handleRepackage(prevPkg)
|
||||
Notifications.setup(this)
|
||||
JobService.schedule(this)
|
||||
Shortcuts.setupDynamic(this)
|
||||
|
||||
// Pre-fetch network services
|
||||
ServiceLocator.networkService
|
||||
}
|
||||
|
||||
private fun handleRepackage(pkg: String?) {
|
||||
if (packageName != APPLICATION_ID) {
|
||||
runCatching {
|
||||
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
|
||||
packageManager.getApplicationInfo(APPLICATION_ID, 0)
|
||||
Shell.cmd("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
} else {
|
||||
if (Config.suManager.isNotEmpty())
|
||||
Config.suManager = ""
|
||||
pkg ?: return
|
||||
if (!Shell.cmd("(pm uninstall $pkg)& >/dev/null 2>&1").exec().isSuccess) {
|
||||
// Uninstall through Android API
|
||||
uninstallAndWait(pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,79 +1,34 @@
|
|||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.Observable
|
||||
import android.os.Bundle
|
||||
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.core.Info
|
||||
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 isConnected get() = Info.isConnected
|
||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||
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
|
||||
private val refreshCallback = object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
requestRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
isConnected.addOnPropertyChangedCallback(refreshCallback)
|
||||
}
|
||||
|
||||
/** 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
|
||||
|
||||
@CallSuper
|
||||
override fun onCleared() {
|
||||
isConnected.removeOnPropertyChangedCallback(refreshCallback)
|
||||
super.onCleared()
|
||||
}
|
||||
open fun onSaveState(state: Bundle) {}
|
||||
open fun onRestoreState(state: Bundle) {}
|
||||
open fun onNetworkChanged(network: Boolean) {}
|
||||
|
||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||
PermissionEvent(permission, callback).publish()
|
||||
|
@ -100,15 +55,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,13 +5,18 @@ import android.graphics.Color
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.res.use
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import rikka.insets.WindowInsetsHelper
|
||||
|
@ -22,12 +27,13 @@ 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
|
||||
|
||||
init {
|
||||
val theme = Config.darkTheme
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -36,7 +42,7 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
startObserveEvents()
|
||||
startObserveLiveData()
|
||||
|
||||
// We need to set the window background explicitly since for whatever reason it's not
|
||||
// propagated upstream
|
||||
|
@ -84,7 +90,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) {
|
||||
|
@ -93,3 +102,14 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun ViewGroup.startAnimations() {
|
||||
val transition = AutoTransition()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.setDuration(400)
|
||||
.excludeTarget(R.id.main_toolbar, true)
|
||||
TransitionManager.beginDelayedTransition(
|
||||
this,
|
||||
transition
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,24 @@
|
|||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||
|
||||
interface ViewModelHolder : LifecycleOwner {
|
||||
interface ViewModelHolder : LifecycleOwner, ViewModelStoreOwner {
|
||||
|
||||
val viewModel: BaseViewModel
|
||||
|
||||
fun startObserveEvents() {
|
||||
viewModel.viewEvents.observe(this) {
|
||||
onEventDispatched(it)
|
||||
}
|
||||
fun startObserveLiveData() {
|
||||
viewModel.viewEvents.observe(this, this::onEventDispatched)
|
||||
Info.isConnected.observe(this, viewModel::onNetworkChanged)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,3 +26,24 @@ interface ViewModelHolder : LifecycleOwner {
|
|||
*/
|
||||
fun onEventDispatched(event: ViewEvent) {}
|
||||
}
|
||||
|
||||
object VMFactory : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return when (modelClass) {
|
||||
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
||||
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
||||
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
||||
InstallViewModel::class.java ->
|
||||
InstallViewModel(ServiceLocator.networkService, ServiceLocator.markwon)
|
||||
SuRequestViewModel::class.java ->
|
||||
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
||||
else -> modelClass.newInstance()
|
||||
} as T
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified VM : ViewModel> ViewModelHolder.viewModel() =
|
||||
lazy(LazyThreadSafetyMode.NONE) {
|
||||
ViewModelProvider(this, VMFactory)[VM::class.java]
|
||||
}
|
||||
|
|
|
@ -1,20 +1,32 @@
|
|||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.system.Os
|
||||
import androidx.profileinstaller.ProfileInstaller
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.utils.*
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.DispatcherExecutor
|
||||
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
||||
import com.topjohnwu.magisk.core.utils.ProcessLifecycle
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.core.utils.ShellInit
|
||||
import com.topjohnwu.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
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
open class App() : Application() {
|
||||
|
@ -22,9 +34,9 @@ open class App() : Application() {
|
|||
constructor(o: Any) : this() {
|
||||
val data = StubApk.Data(o)
|
||||
// Add the root service name mapping
|
||||
data.classToComponent[RootRegistry::class.java.name] = data.rootService.name
|
||||
data.classToComponent[RootUtils::class.java.name] = data.rootService.name
|
||||
// Send back the actual root service class
|
||||
data.rootService = RootRegistry::class.java
|
||||
data.rootService = RootUtils::class.java
|
||||
Info.stub = data
|
||||
}
|
||||
|
||||
|
@ -35,46 +47,55 @@ open class App() : Application() {
|
|||
Timber.e(e)
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
Os.setenv("PATH", "${Os.getenv("PATH")}:/debug_ramdisk:/sbin", true)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(context: Context) {
|
||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
.setInitializers(ShellInit::class.java)
|
||||
.setTimeout(2))
|
||||
Shell.EXECUTOR = DispatcherExecutor(Dispatchers.IO)
|
||||
|
||||
// Get the actual ContextImpl
|
||||
val app: Application
|
||||
val base: Context
|
||||
if (context is Application) {
|
||||
app = context
|
||||
base = context.baseContext
|
||||
AppApkPath = StubApk.current(base).path
|
||||
} else {
|
||||
app = this
|
||||
base = context
|
||||
AppApkPath = base.packageResourcePath
|
||||
}
|
||||
super.attachBaseContext(base)
|
||||
ServiceLocator.context = base
|
||||
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
||||
|
||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
.setInitializers(ShellInit::class.java)
|
||||
.setContext(base)
|
||||
.setTimeout(2))
|
||||
Shell.EXECUTOR = DispatcherExecutor(Dispatchers.IO)
|
||||
RootUtils.bindTask = RootService.bindOrTask(
|
||||
intent<RootUtils>(),
|
||||
UiThreadHandler.executor,
|
||||
RootUtils.Connection
|
||||
)
|
||||
// Pre-heat the shell ASAP
|
||||
Shell.getShell(null) {}
|
||||
|
||||
refreshLocale()
|
||||
AppApkPath = if (isRunningAsStub) {
|
||||
StubApk.current(base).path
|
||||
} else {
|
||||
base.packageResourcePath
|
||||
}
|
||||
|
||||
base.resources.patch()
|
||||
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
||||
resources.patch()
|
||||
Notifications.setup()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
RootRegistry.bindTask = RootService.bindOrTask(
|
||||
intent<RootRegistry>(),
|
||||
UiThreadHandler.executor,
|
||||
RootRegistry.Connection
|
||||
)
|
||||
ProcessLifecycle.init(this)
|
||||
NetworkObserver.init(this)
|
||||
if (!BuildConfig.DEBUG && !isRunningAsStub) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ProfileInstaller.writeProfile(this@App)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
|
@ -86,20 +107,21 @@ open class App() : Application() {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
val foreground: Activity? get() = ref.get()
|
||||
|
||||
@Volatile
|
||||
var foreground: Activity? = null
|
||||
private var ref = WeakReference<Activity>(null)
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
if (activity is SuRequestActivity) return
|
||||
foreground = activity
|
||||
ref = WeakReference(activity)
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
if (activity is SuRequestActivity) return
|
||||
foreground = null
|
||||
ref.clear()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
|
|
|
@ -1,34 +1,36 @@
|
|||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
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.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.repository.BoolDBPropertyNoWrite
|
||||
import com.topjohnwu.magisk.core.repository.DBConfig
|
||||
import com.topjohnwu.magisk.core.repository.PreferenceConfig
|
||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.data.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.data.repository.DBBoolSettingsNoWrite
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.IOException
|
||||
|
||||
object Config : PreferenceModel, DBConfig {
|
||||
object Config : PreferenceConfig, DBConfig {
|
||||
|
||||
override val stringDB get() = ServiceLocator.stringDB
|
||||
override val settingsDB get() = ServiceLocator.settingsDB
|
||||
override val context get() = ServiceLocator.deContext
|
||||
override val coroutineScope get() = GlobalScope
|
||||
|
||||
@get:SuppressLint("ApplySharedPref")
|
||||
val prefsFile: File get() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().apply {
|
||||
remove(Key.ASKED_HOME)
|
||||
}.commit()
|
||||
return File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||
private val prefsFile = File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
fun getPrefsFile(): File {
|
||||
prefs.edit().remove(Key.ASKED_HOME).commit()
|
||||
return prefsFile
|
||||
}
|
||||
|
||||
object Key {
|
||||
|
@ -38,6 +40,7 @@ object Config : PreferenceModel, DBConfig {
|
|||
const val SU_MNT_NS = "mnt_ns"
|
||||
const val SU_BIOMETRIC = "su_biometric"
|
||||
const val ZYGISK = "zygisk"
|
||||
const val BOOTLOOP = "bootloop"
|
||||
const val DENYLIST = "denylist"
|
||||
const val SU_MANAGER = "requester"
|
||||
const val KEYSTORE = "keystore"
|
||||
|
@ -70,6 +73,7 @@ object Config : PreferenceModel, DBConfig {
|
|||
const val BETA_CHANNEL = 1
|
||||
const val CUSTOM_CHANNEL = 2
|
||||
const val CANARY_CHANNEL = 3
|
||||
const val DEBUG_CHANNEL = 4
|
||||
|
||||
// root access mode
|
||||
const val ROOT_ACCESS_DISABLED = 0
|
||||
|
@ -106,13 +110,14 @@ object Config : PreferenceModel, DBConfig {
|
|||
|
||||
private val defaultChannel =
|
||||
if (BuildConfig.DEBUG)
|
||||
Value.DEBUG_CHANNEL
|
||||
else if (Const.APP_IS_CANARY)
|
||||
Value.CANARY_CHANNEL
|
||||
else
|
||||
Value.DEFAULT_CHANNEL
|
||||
|
||||
@JvmField var keepVerity = false
|
||||
@JvmField var keepEnc = false
|
||||
@JvmField var patchVbmeta = false
|
||||
@JvmField var recovery = false
|
||||
|
||||
var bootId by preference(Key.BOOT_ID, "")
|
||||
|
@ -131,7 +136,15 @@ object Config : PreferenceModel, 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)
|
||||
|
||||
|
@ -147,9 +160,15 @@ object Config : PreferenceModel, DBConfig {
|
|||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||
private var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||
var userAuth
|
||||
get() = Info.isDeviceSecure && suBiometric
|
||||
set(value) {
|
||||
suBiometric = value
|
||||
}
|
||||
var zygisk by dbSettings(Key.ZYGISK, false)
|
||||
var denyList by DBBoolSettingsNoWrite(Key.DENYLIST, false)
|
||||
var bootloop by dbSettings(Key.BOOTLOOP, 0)
|
||||
var denyList by BoolDBPropertyNoWrite(Key.DENYLIST, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||
|
||||
|
@ -157,10 +176,15 @@ object Config : PreferenceModel, DBConfig {
|
|||
|
||||
fun load(pkg: String?) {
|
||||
// Only try to load prefs when fresh install and a previous package name is set
|
||||
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
||||
context.contentResolver.openInputStream(Provider.PREFS_URI(pkg))?.use {
|
||||
prefs.edit { parsePrefs(it) }
|
||||
if (pkg != null && prefs.all.isEmpty()) {
|
||||
runBlocking {
|
||||
try {
|
||||
context.contentResolver
|
||||
.openInputStream(Provider.preferencesUri(pkg))
|
||||
?.writeTo(prefsFile, dispatcher = Dispatchers.Unconfined)
|
||||
} catch (ignored: IOException) {}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
prefs.edit {
|
||||
|
@ -169,57 +193,10 @@ object Config : PreferenceModel, DBConfig {
|
|||
suBiometric = true
|
||||
remove(SU_FINGERPRINT)
|
||||
prefs.getString(Key.UPDATE_CHANNEL, null).also {
|
||||
if (it == null)
|
||||
if (it == null ||
|
||||
it.toInt() > Value.DEBUG_CHANNEL ||
|
||||
it.toInt() < Value.DEFAULT_CHANNEL) {
|
||||
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
||||
else if (it.toInt() > Value.CANARY_CHANNEL)
|
||||
putString(Key.UPDATE_CHANNEL, Value.CANARY_CHANNEL.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.parsePrefs(input: InputStream) {
|
||||
runCatching {
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.START_TAG, null, "map")
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG)
|
||||
continue
|
||||
val key: String = parser.getAttributeValue(null, "name")
|
||||
fun value() = parser.getAttributeValue(null, "value")!!
|
||||
when (parser.name) {
|
||||
"string" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string")
|
||||
putString(key, parser.nextText())
|
||||
parser.require(XmlPullParser.END_TAG, null, "string")
|
||||
}
|
||||
"boolean" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean")
|
||||
putBoolean(key, value().toBoolean())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean")
|
||||
}
|
||||
"int" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putInt(key, value().toInt())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
"long" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "long")
|
||||
putLong(key, value().toLong())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "long")
|
||||
}
|
||||
"float" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putFloat(key, value().toFloat())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
else -> parser.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
@ -25,18 +24,19 @@ object Const {
|
|||
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.VERSION_CODE)
|
||||
|
||||
object Version {
|
||||
const val MIN_VERSION = "v21.0"
|
||||
const val MIN_VERCODE = 21000
|
||||
const val MIN_VERSION = "v22.0"
|
||||
const val MIN_VERCODE = 22000
|
||||
|
||||
fun atLeast_21_2() = Info.env.versionCode >= 21200 || isCanary()
|
||||
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
|
||||
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
|
||||
fun isCanary() = isCanary(Info.env.versionCode)
|
||||
|
||||
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
||||
}
|
||||
|
||||
object ID {
|
||||
const val JOB_SERVICE_ID = 7
|
||||
const val DOWNLOAD_JOB_ID = 6
|
||||
const val CHECK_UPDATE_JOB_ID = 7
|
||||
}
|
||||
|
||||
object Url {
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
|
@ -13,45 +12,49 @@ import android.content.res.Resources
|
|||
import android.util.DisplayMetrics
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.unwrap
|
||||
import com.topjohnwu.magisk.core.utils.syncLocale
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
|
||||
lateinit var AppApkPath: String
|
||||
|
||||
fun AssetManager.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
|
||||
|
||||
fun Context.wrap(): Context = if (this is PatchedContext) this else PatchedContext(this)
|
||||
|
||||
private class PatchedContext(base: Context) : ContextWrapper(base) {
|
||||
init { base.resources.patch() }
|
||||
override fun getClassLoader() = javaClass.classLoader!!
|
||||
override fun createConfigurationContext(config: Configuration) =
|
||||
super.createConfigurationContext(config).wrap()
|
||||
}
|
||||
fun Resources.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
|
||||
|
||||
fun Resources.patch(): Resources {
|
||||
syncLocale()
|
||||
if (isRunningAsStub)
|
||||
assets.addAssetPath(AppApkPath)
|
||||
addAssetPath(AppApkPath)
|
||||
syncLocale()
|
||||
return this
|
||||
}
|
||||
|
||||
fun Context.patch(): Context {
|
||||
unwrap().resources.patch()
|
||||
return this
|
||||
}
|
||||
|
||||
// Wrapping is only necessary for ContextThemeWrapper to support configuration overrides
|
||||
fun Context.wrap(): Context {
|
||||
patch()
|
||||
return object : ContextWrapper(this) {
|
||||
override fun createConfigurationContext(config: Configuration): Context {
|
||||
return super.createConfigurationContext(config).wrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createNewResources(): Resources {
|
||||
val asset = AssetManager::class.java.newInstance()
|
||||
asset.addAssetPath(AppApkPath)
|
||||
val config = Configuration(AppContext.resources.configuration)
|
||||
val metrics = DisplayMetrics()
|
||||
metrics.setTo(AppContext.resources.displayMetrics)
|
||||
return Resources(asset, metrics, config)
|
||||
val res = Resources(asset, metrics, config)
|
||||
res.addAssetPath(AppApkPath)
|
||||
return res
|
||||
}
|
||||
|
||||
fun Class<*>.cmp(pkg: String) =
|
||||
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||
|
||||
inline fun <reified T> Activity.redirect() = Intent(intent)
|
||||
.setComponent(T::class.java.cmp(packageName))
|
||||
.setFlags(0)
|
||||
|
||||
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||
|
||||
// Keep a reference to these resources to prevent it from
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.os.Build
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import android.app.KeyguardManager
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.getProperty
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.getProperty
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
|
||||
val isRunningAsStub get() = Info.stub != null
|
||||
|
||||
|
@ -29,42 +26,50 @@ object Info {
|
|||
// Device state
|
||||
@JvmStatic val env by lazy { loadState() }
|
||||
@JvmField var isSAR = false
|
||||
var legacySAR = false
|
||||
var isAB = false
|
||||
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
||||
@JvmStatic val isFDE get() = crypto == "block"
|
||||
@JvmField var ramdisk = false
|
||||
@JvmField var vbmeta = false
|
||||
var patchBootVbmeta = false
|
||||
var crypto = ""
|
||||
var noDataExec = false
|
||||
var isRooted = false
|
||||
|
||||
@JvmField var hasGMS = true
|
||||
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||
@JvmField val isEmulator =
|
||||
getProperty("ro.kernel.qemu", "0") == "1" ||
|
||||
getProperty("ro.boot.qemu", "0") == "1"
|
||||
|
||||
val isConnected by lazy {
|
||||
ObservableBoolean(false).also { field ->
|
||||
NetworkObserver.observe(AppContext) {
|
||||
UiThreadHandler.run { field.set(it) }
|
||||
}
|
||||
}
|
||||
val isConnected = MutableLiveData(false)
|
||||
|
||||
val showSuperUser: Boolean get() {
|
||||
return env.isActive && (Const.USER_ID == 0
|
||||
|| Config.suMultiuserMode == Config.Value.MULTIUSER_MODE_USER)
|
||||
}
|
||||
|
||||
private fun loadState() = Env(
|
||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
||||
)
|
||||
val isDeviceSecure get() =
|
||||
AppContext.getSystemService(KeyguardManager::class.java).isDeviceSecure
|
||||
|
||||
private fun loadState(): Env {
|
||||
val v = fastCmd("magisk -v").split(":".toRegex())
|
||||
return Env(
|
||||
v[0], v.size >= 3 && v[2] == "D",
|
||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
||||
)
|
||||
}
|
||||
|
||||
class Env(
|
||||
val versionString: String = "",
|
||||
val isDebug: Boolean = false,
|
||||
code: Int = -1
|
||||
) {
|
||||
val versionCode = when {
|
||||
code < Const.Version.MIN_VERCODE -> -1
|
||||
else -> if (Shell.rootAccess()) code else -1
|
||||
isRooted -> code
|
||||
else -> -1
|
||||
}
|
||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||
val isActive = versionCode >= 0
|
||||
val isActive = versionCode > 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Notification
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobParameters
|
||||
import android.app.job.JobScheduler
|
||||
|
@ -7,39 +10,79 @@ import android.content.Context
|
|||
import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.base.BaseJobService
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class JobService : BaseJobService() {
|
||||
|
||||
private val job = Job()
|
||||
private val svc get() = ServiceLocator.networkService
|
||||
private var mSession: Session? = null
|
||||
|
||||
override fun onStartJob(params: JobParameters): Boolean {
|
||||
val coroutineScope = CoroutineScope(Dispatchers.IO + job)
|
||||
coroutineScope.launch {
|
||||
doWork()
|
||||
@TargetApi(value = 34)
|
||||
inner class Session(
|
||||
private var params: JobParameters
|
||||
) : DownloadEngine.Session {
|
||||
|
||||
override val context get() = this@JobService
|
||||
val engine = DownloadEngine(this)
|
||||
|
||||
fun updateParams(params: JobParameters) {
|
||||
this.params = params
|
||||
engine.reattach()
|
||||
}
|
||||
|
||||
override fun attachNotification(id: Int, builder: Notification.Builder) {
|
||||
setNotification(params, id, builder.build(), JOB_END_NOTIFICATION_POLICY_REMOVE)
|
||||
}
|
||||
|
||||
override fun onDownloadComplete() {
|
||||
jobFinished(params, false)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun doWork() {
|
||||
svc.fetchUpdate()?.let {
|
||||
Info.remote = it
|
||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
||||
Notifications.updateAvailable(this)
|
||||
@SuppressLint("NewApi")
|
||||
override fun onStartJob(params: JobParameters): Boolean {
|
||||
return when (params.jobId) {
|
||||
Const.ID.CHECK_UPDATE_JOB_ID -> checkUpdate(params)
|
||||
Const.ID.DOWNLOAD_JOB_ID -> downloadFile(params)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters): Boolean {
|
||||
job.cancel()
|
||||
return false
|
||||
override fun onStopJob(params: JobParameters?) = false
|
||||
|
||||
@TargetApi(value = 34)
|
||||
private fun downloadFile(params: JobParameters): Boolean {
|
||||
params.transientExtras.classLoader = Subject::class.java.classLoader
|
||||
val subject = params.transientExtras
|
||||
.getParcelable(DownloadEngine.SUBJECT_KEY, Subject::class.java) ?:
|
||||
return false
|
||||
|
||||
val session = mSession?.also {
|
||||
it.updateParams(params)
|
||||
} ?: run {
|
||||
Session(params).also { mSession = it }
|
||||
}
|
||||
|
||||
session.engine.download(subject)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun checkUpdate(params: JobParameters): Boolean {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ServiceLocator.networkService.fetchUpdate()?.let {
|
||||
Info.remote = it
|
||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
||||
Notifications.updateAvailable()
|
||||
jobFinished(params, false)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -47,14 +90,14 @@ class JobService : BaseJobService() {
|
|||
val scheduler = context.getSystemService<JobScheduler>() ?: return
|
||||
if (Config.checkUpdate) {
|
||||
val cmp = JobService::class.java.cmp(context.packageName)
|
||||
val info = JobInfo.Builder(Const.ID.JOB_SERVICE_ID, cmp)
|
||||
val info = JobInfo.Builder(Const.ID.CHECK_UPDATE_JOB_ID, cmp)
|
||||
.setPeriodic(TimeUnit.HOURS.toMillis(12))
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
scheduler.schedule(info)
|
||||
} else {
|
||||
scheduler.cancel(Const.ID.JOB_SERVICE_ID)
|
||||
scheduler.cancel(Const.ID.CHECK_UPDATE_JOB_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,34 @@
|
|||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.pm.ProviderInfo
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import java.io.File
|
||||
import com.topjohnwu.magisk.core.su.TestHandler
|
||||
|
||||
class Provider : ContentProvider() {
|
||||
|
||||
override fun attachInfo(context: Context, info: ProviderInfo) {
|
||||
super.attachInfo(context.wrap(), info)
|
||||
}
|
||||
class Provider : BaseProvider() {
|
||||
|
||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
SuCallbackHandler.run(context!!, method, extras)
|
||||
return Bundle.EMPTY
|
||||
return when (method) {
|
||||
SuCallbackHandler.LOG, SuCallbackHandler.NOTIFY -> {
|
||||
SuCallbackHandler.run(context!!, method, extras)
|
||||
Bundle.EMPTY
|
||||
}
|
||||
else -> TestHandler.run(method)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
return when (uri.encodedPath ?: return null) {
|
||||
"/apk_file" -> ParcelFileDescriptor.open(File(context!!.packageCodePath), MODE_READ_ONLY)
|
||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.getPrefsFile(), MODE_READ_ONLY)
|
||||
else -> super.openFile(uri, mode)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun APK_URI(pkg: String) =
|
||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("apk_file").build()
|
||||
|
||||
fun PREFS_URI(pkg: String) =
|
||||
fun preferencesUri(pkg: String): Uri =
|
||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
||||
}
|
||||
|
||||
override fun onCreate() = true
|
||||
override fun getType(uri: Uri): String? = null
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.IntentCompat
|
||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
@ -26,14 +29,21 @@ open class Receiver : BaseReceiver() {
|
|||
return if (uid == -1) null else uid
|
||||
}
|
||||
|
||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
intent ?: return
|
||||
super.onReceive(context, intent)
|
||||
|
||||
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
||||
policyDB.delete(uid)
|
||||
}
|
||||
|
||||
when (intent.action ?: return) {
|
||||
DownloadEngine.ACTION -> {
|
||||
IntentCompat.getParcelableExtra(
|
||||
intent, DownloadEngine.SUBJECT_KEY, Subject::class.java)?.let {
|
||||
DownloadEngine.start(context, it)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
// This will only work pre-O
|
||||
if (Config.suReAuth)
|
||||
|
@ -50,7 +60,7 @@ open class Receiver : BaseReceiver() {
|
|||
@Suppress("DEPRECATION")
|
||||
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
||||
if (installer == context.packageName) {
|
||||
Notifications.updateDone(context)
|
||||
Notifications.updateDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.IntentCompat
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
|
||||
class Service : BaseService(), DownloadEngine.Session {
|
||||
|
||||
private var mEngine: DownloadEngine? = null
|
||||
override val context get() = this
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
if (intent.action == DownloadEngine.ACTION) {
|
||||
IntentCompat
|
||||
.getParcelableExtra(intent, DownloadEngine.SUBJECT_KEY, Subject::class.java)
|
||||
?.let { subject ->
|
||||
val engine = mEngine ?: DownloadEngine(this).also { mEngine = it }
|
||||
engine.download(subject)
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun attachNotification(id: Int, builder: Notification.Builder) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
|
||||
startForeground(id, builder.build())
|
||||
}
|
||||
|
||||
override fun onDownloadComplete() {
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
|
@ -1,25 +1,34 @@
|
|||
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
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts.GetContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.reflectField
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.utils.RequestAuthentication
|
||||
import com.topjohnwu.magisk.core.utils.RequestInstall
|
||||
import com.topjohnwu.magisk.core.utils.UninstallPackage
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.ktx.reflectField
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
||||
fun onActivityLaunch() {}
|
||||
// Make the result type explicitly non-null
|
||||
override fun onActivityResult(result: Uri)
|
||||
}
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
|
@ -28,26 +37,33 @@ 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: ((Uri) -> Unit)? = null
|
||||
var authenticateCallback: ((Boolean) -> Unit)? = null
|
||||
val requestAuthenticate = registerForActivityResult(RequestAuthentication()) {
|
||||
authenticateCallback?.invoke(it)
|
||||
authenticateCallback = null
|
||||
}
|
||||
|
||||
private var contentCallback: ContentResultCallback? = null
|
||||
private val getContent = registerForActivityResult(GetContent()) {
|
||||
if (it != null) contentCallback?.invoke(it)
|
||||
if (it != null) contentCallback?.onActivityResult(it)
|
||||
contentCallback = null
|
||||
}
|
||||
|
||||
private var uninstallLatch = CountDownLatch(1)
|
||||
private val uninstallPkg = registerForActivityResult(UninstallPackage()) {
|
||||
uninstallLatch.countDown()
|
||||
private val mReferrerField by lazy(LazyThreadSafetyMode.NONE) {
|
||||
Activity::class.java.reflectField("mReferrer")
|
||||
}
|
||||
|
||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||
// Force applying our preferred local
|
||||
config?.setLocale(currentLocale)
|
||||
super.applyOverrideConfiguration(config)
|
||||
val realCallingPackage: String? get() {
|
||||
callingPackage?.let { return it }
|
||||
mReferrerField.get(this)?.let { return it as String }
|
||||
return null
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
|
@ -59,36 +75,50 @@ 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)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
contentCallback?.let {
|
||||
outState.putParcelable(CONTENT_CALLBACK_KEY, it)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun getContent(type: String, callback: (Uri) -> Unit) {
|
||||
fun getContent(type: String, callback: ContentResultCallback) {
|
||||
contentCallback = callback
|
||||
getContent.launch(type)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun uninstallAndWait(pkg: String) {
|
||||
uninstallLatch = CountDownLatch(1)
|
||||
uninstallPkg.launch(pkg)
|
||||
uninstallLatch.await(3, TimeUnit.SECONDS)
|
||||
try {
|
||||
getContent.launch(type)
|
||||
callback.onActivityLaunch()
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun recreate() {
|
||||
|
@ -100,4 +130,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||
startActivity(Intent(intent).setFlags(0))
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_CALLBACK_KEY = "content_callback"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@ package com.topjohnwu.magisk.core.base
|
|||
|
||||
import android.app.job.JobService
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.core.patch
|
||||
|
||||
abstract class BaseJobService : JobService() {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
super.attachBaseContext(base.patch())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.pm.ProviderInfo
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.core.patch
|
||||
|
||||
open class BaseProvider : ContentProvider() {
|
||||
override fun attachInfo(context: Context, info: ProviderInfo) {
|
||||
super.attachInfo(context.patch(), info)
|
||||
}
|
||||
override fun onCreate() = true
|
||||
override fun getType(uri: Uri): String? = null
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
|
||||
}
|
|
@ -2,15 +2,13 @@ package com.topjohnwu.magisk.core.base
|
|||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import androidx.annotation.CallSuper
|
||||
import com.topjohnwu.magisk.core.patch
|
||||
|
||||
abstract class BaseReceiver : BroadcastReceiver() {
|
||||
|
||||
final override fun onReceive(context: Context, intent: Intent?) {
|
||||
onReceive(context.wrap() as ContextWrapper, intent)
|
||||
@CallSuper
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
context.patch()
|
||||
}
|
||||
|
||||
abstract fun onReceive(context: ContextWrapper, intent: Intent?)
|
||||
}
|
||||
|
|
|
@ -2,10 +2,13 @@ package com.topjohnwu.magisk.core.base
|
|||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.magisk.core.patch
|
||||
|
||||
abstract class BaseService : Service() {
|
||||
open class BaseService : Service() {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
super.attachBaseContext(base.patch())
|
||||
}
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
}
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
package com.topjohnwu.magisk.data.network
|
||||
package com.topjohnwu.magisk.core.data
|
||||
|
||||
import com.topjohnwu.magisk.core.model.BranchInfo
|
||||
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.*
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Streaming
|
||||
import retrofit2.http.Url
|
||||
|
||||
private const val BRANCH = "branch"
|
||||
private const val REPO = "repo"
|
||||
|
@ -12,15 +16,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
|
|
@ -1,15 +1,31 @@
|
|||
package com.topjohnwu.magisk.data.database
|
||||
package com.topjohnwu.magisk.core.data
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Database
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
|
||||
@Database(version = 1, entities = [SuLog::class], exportSchema = false)
|
||||
@Database(version = 2, entities = [SuLog::class], exportSchema = false)
|
||||
abstract class SuLogDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun suLogDao(): SuLogDao
|
||||
|
||||
companion object {
|
||||
val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) = with(database) {
|
||||
execSQL("ALTER TABLE logs ADD COLUMN target INTEGER NOT NULL DEFAULT -1")
|
||||
execSQL("ALTER TABLE logs ADD COLUMN context TEXT NOT NULL DEFAULT ''")
|
||||
execSQL("ALTER TABLE logs ADD COLUMN gids TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
|
@ -0,0 +1,47 @@
|
|||
package com.topjohnwu.magisk.core.data.magiskdb
|
||||
|
||||
import com.topjohnwu.magisk.core.ktx.await
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
open class MagiskDB {
|
||||
|
||||
suspend fun <R> exec(
|
||||
query: String,
|
||||
mapper: suspend (Map<String, String>) -> R
|
||||
): List<R> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val out = Shell.cmd("magisk --sqlite '$query'").await().out
|
||||
out.map { line ->
|
||||
line.split("\\|".toRegex())
|
||||
.map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.associate { it[0] to it[1] }
|
||||
.let { mapper(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun exec(query: String) {
|
||||
exec(query) {}
|
||||
}
|
||||
|
||||
fun Map<String, Any>.toQuery(): String {
|
||||
val keys = this.keys.joinToString(",")
|
||||
val values = this.values.joinToString(",") {
|
||||
when (it) {
|
||||
is Boolean -> if (it) "1" else "0"
|
||||
is Number -> it.toString()
|
||||
else -> "\"$it\""
|
||||
}
|
||||
}
|
||||
return "($keys) VALUES($values)"
|
||||
}
|
||||
|
||||
object Table {
|
||||
const val POLICY = "policies"
|
||||
const val SETTINGS = "settings"
|
||||
const val STRINGS = "strings"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package com.topjohnwu.magisk.core.data.magiskdb
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class PolicyDao : MagiskDB() {
|
||||
|
||||
suspend fun deleteOutdated() {
|
||||
val nowSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
|
||||
val query = "DELETE FROM ${Table.POLICY} WHERE " +
|
||||
"(until > 0 AND until < $nowSeconds) OR until < 0"
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun delete(uid: Int) {
|
||||
val query = "DELETE FROM ${Table.POLICY} WHERE uid == $uid"
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun fetch(uid: Int): SuPolicy? {
|
||||
val query = "SELECT * FROM ${Table.POLICY} WHERE uid == $uid LIMIT = 1"
|
||||
return exec(query, ::toPolicy).firstOrNull()
|
||||
}
|
||||
|
||||
suspend fun update(policy: SuPolicy) {
|
||||
val map = policy.toMap()
|
||||
if (!Const.Version.atLeast_25_0()) {
|
||||
// Put in package_name for old database
|
||||
map["package_name"] = AppContext.packageManager.getNameForUid(policy.uid)!!
|
||||
}
|
||||
val query = "REPLACE INTO ${Table.POLICY} ${map.toQuery()}"
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun fetchAll(): List<SuPolicy> {
|
||||
val query = "SELECT * FROM ${Table.POLICY} WHERE uid/100000 == ${Const.USER_ID}"
|
||||
return exec(query, ::toPolicy).filterNotNull()
|
||||
}
|
||||
|
||||
private fun toPolicy(map: Map<String, String>): SuPolicy? {
|
||||
val uid = map["uid"]?.toInt() ?: return null
|
||||
val policy = SuPolicy(uid)
|
||||
|
||||
map["policy"]?.toInt()?.let { policy.policy = it }
|
||||
map["until"]?.toLong()?.let { policy.until = it }
|
||||
map["logging"]?.toInt()?.let { policy.logging = it != 0 }
|
||||
map["notification"]?.toInt()?.let { policy.notification = it != 0 }
|
||||
return policy
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.topjohnwu.magisk.core.data.magiskdb
|
||||
|
||||
class SettingsDao : MagiskDB() {
|
||||
|
||||
suspend fun delete(key: String) {
|
||||
val query = "DELETE FROM ${Table.SETTINGS} WHERE key == \"$key\""
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun put(key: String, value: Int) {
|
||||
val kv = mapOf("key" to key, "value" to value)
|
||||
val query = "REPLACE INTO ${Table.SETTINGS} ${kv.toQuery()}"
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun fetch(key: String, default: Int = -1): Int {
|
||||
val query = "SELECT value FROM ${Table.SETTINGS} WHERE key == \"$key\" LIMIT 1"
|
||||
return exec(query) { it["value"]?.toInt() }.firstOrNull() ?: default
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.topjohnwu.magisk.core.data.magiskdb
|
||||
|
||||
class StringDao : MagiskDB() {
|
||||
|
||||
suspend fun delete(key: String) {
|
||||
val query = "DELETE FROM ${Table.STRINGS} WHERE key == \"$key\""
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun put(key: String, value: String) {
|
||||
val kv = mapOf("key" to key, "value" to value)
|
||||
val query = "REPLACE INTO ${Table.STRINGS} ${kv.toQuery()}"
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun fetch(key: String, default: String = ""): String {
|
||||
val query = "SELECT value FROM ${Table.STRINGS} WHERE key == \"$key\" LIMIT 1"
|
||||
return exec(query) { it["value"] }.firstOrNull() ?: default
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.topjohnwu.magisk.di
|
||||
package com.topjohnwu.magisk.core.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
|
@ -1,25 +1,17 @@
|
|||
package com.topjohnwu.magisk.di
|
||||
package com.topjohnwu.magisk.core.di
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.text.method.LinkMovementMethod
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.room.Room
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.data.database.SuLogDatabase
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||
import com.topjohnwu.magisk.core.data.SuLogDatabase
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||
|
||||
|
@ -47,32 +39,13 @@ object ServiceLocator {
|
|||
NetworkService(
|
||||
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
||||
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
||||
createApiService(retrofit, Const.Url.GITHUB_API_URL)
|
||||
)
|
||||
}
|
||||
|
||||
object VMFactory : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return when (modelClass) {
|
||||
HomeViewModel::class.java -> HomeViewModel(networkService)
|
||||
LogViewModel::class.java -> LogViewModel(logRepo)
|
||||
SuperuserViewModel::class.java -> SuperuserViewModel(policyDB)
|
||||
InstallViewModel::class.java -> InstallViewModel(networkService)
|
||||
SuRequestViewModel::class.java -> SuRequestViewModel(policyDB, timeoutPrefs)
|
||||
else -> modelClass.newInstance()
|
||||
} as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified VM : ViewModel> ViewModelStoreOwner.viewModel() =
|
||||
lazy(LazyThreadSafetyMode.NONE) {
|
||||
ViewModelProvider(this, ServiceLocator.VMFactory)[VM::class.java]
|
||||
}
|
||||
|
||||
private fun createSuLogDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||
.addMigrations(SuLogDatabase.MIGRATION_1_2)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.collection.isNotEmpty
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.ActivityTracker
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.JobService
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.cmp
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.core.ktx.forEach
|
||||
import com.topjohnwu.magisk.core.ktx.set
|
||||
import com.topjohnwu.magisk.core.ktx.withStreams
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.ResponseBody
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
/**
|
||||
* This class drives the execution of file downloads and notification management.
|
||||
*
|
||||
* Each download engine instance has to be paired with a "session" that is managed by the operating
|
||||
* system. A session is an Android component that allows executing long lasting operations and
|
||||
* have its state tied to a notification to show progress.
|
||||
*
|
||||
* A session can only have one single notification representing its state, and the operating system
|
||||
* also uses the notification to manage the lifecycle of a session. One goal of this class is
|
||||
* to support concurrent download tasks using only one single session, so internally it manages
|
||||
* all active tasks and notifications and properly re-assign notifications to be attached to
|
||||
* the session to make sure all download operations can be completed without the operating system
|
||||
* killing the session.
|
||||
*
|
||||
* For API 23 - 33, we use a foreground service as a session.
|
||||
* For API 34 and higher, we use user-initiated job services as a session.
|
||||
*/
|
||||
class DownloadEngine(
|
||||
private val session: Session
|
||||
) {
|
||||
|
||||
interface Session {
|
||||
val context: Context
|
||||
|
||||
fun attachNotification(id: Int, builder: Notification.Builder)
|
||||
fun onDownloadComplete()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION = "com.topjohnwu.magisk.DOWNLOAD"
|
||||
const val SUBJECT_KEY = "subject"
|
||||
private const val REQUEST_CODE = 1
|
||||
|
||||
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
||||
|
||||
private fun broadcast(progress: Float, subject: Subject) {
|
||||
progressBroadcast.postValue(progress to subject)
|
||||
}
|
||||
|
||||
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
||||
progressBroadcast.value = null
|
||||
progressBroadcast.observe(owner) {
|
||||
val (progress, subject) = it ?: return@observe
|
||||
callback(progress, subject)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createIntent(context: Context, subject: Subject) =
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
context.intent<com.topjohnwu.magisk.core.Receiver>()
|
||||
.setAction(ACTION)
|
||||
.putExtra(SUBJECT_KEY, subject)
|
||||
} else {
|
||||
context.intent<com.topjohnwu.magisk.core.Service>()
|
||||
.setAction(ACTION)
|
||||
.putExtra(SUBJECT_KEY, subject)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||
val flag = PendingIntent.FLAG_IMMUTABLE or
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
PendingIntent.FLAG_ONE_SHOT
|
||||
val intent = createIntent(context, subject)
|
||||
return if (Build.VERSION.SDK_INT >= 34) {
|
||||
// On API 34+, download tasks are handled with a user-initiated job.
|
||||
// However, there is no way to schedule a new job directly with a pending intent.
|
||||
// As a workaround, we send the subject to a broadcast receiver and have it
|
||||
// schedule the job for us.
|
||||
PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flag)
|
||||
} else if (Build.VERSION.SDK_INT >= 26) {
|
||||
PendingIntent.getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||
} else {
|
||||
PendingIntent.getService(context, REQUEST_CODE, intent, flag)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun startWithActivity(activity: BaseActivity, subject: Subject) {
|
||||
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||
// Always download regardless of notification permission status
|
||||
start(activity.applicationContext, subject)
|
||||
}
|
||||
}
|
||||
|
||||
fun start(context: Context, subject: Subject) {
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
val scheduler = context.getSystemService<JobScheduler>()!!
|
||||
val cmp = JobService::class.java.cmp(context.packageName)
|
||||
val extras = Bundle()
|
||||
extras.putParcelable(SUBJECT_KEY, subject)
|
||||
val info = JobInfo.Builder(Const.ID.DOWNLOAD_JOB_ID, cmp)
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
.setUserInitiated(true)
|
||||
.setTransientExtras(extras)
|
||||
.build()
|
||||
scheduler.schedule(info)
|
||||
} else if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.startForegroundService(createIntent(context, subject))
|
||||
} else {
|
||||
context.startService(createIntent(context, subject))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun download(subject: Subject) {
|
||||
notifyUpdate(subject.notifyId)
|
||||
CoroutineScope(job + Dispatchers.IO).launch {
|
||||
try {
|
||||
val stream = network.fetchFile(subject.url).toProgressStream(subject)
|
||||
when (subject) {
|
||||
is Subject.App -> handleApp(stream, subject)
|
||||
is Subject.Module -> handleModule(stream, subject.file)
|
||||
else -> stream.copyAndClose(subject.file.outputStream())
|
||||
}
|
||||
val activity = ActivityTracker.foreground
|
||||
if (activity != null && subject.autoLaunch) {
|
||||
notifyRemove(subject.notifyId)
|
||||
subject.pendingIntent(activity)?.send()
|
||||
} else {
|
||||
notifyFinish(subject)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
notifyFail(subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun reattach() {
|
||||
val builder = notifications[attachedId] ?: return
|
||||
session.attachNotification(attachedId, builder)
|
||||
}
|
||||
|
||||
private val notifications = SparseArrayCompat<Notification.Builder>()
|
||||
private var attachedId = -1
|
||||
|
||||
private val job = Job()
|
||||
|
||||
private val context get() = session.context
|
||||
private val network get() = ServiceLocator.networkService
|
||||
|
||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
||||
val newId = Notifications.nextId()
|
||||
Notifications.mgr.notify(newId, notification.build())
|
||||
return newId
|
||||
}
|
||||
|
||||
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
|
||||
broadcast(-2f, subject)
|
||||
it.setContentText(context.getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
|
||||
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
|
||||
broadcast(1f, subject)
|
||||
it.setContentTitle(subject.title)
|
||||
.setContentText(context.getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) }
|
||||
}
|
||||
|
||||
private fun attachNotification(id: Int, notification: Notification.Builder) {
|
||||
attachedId = id
|
||||
session.attachNotification(id, notification)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
val notification = (notifications[id] ?: Notifications.startProgress("").also {
|
||||
notifications[id] = it
|
||||
}).apply(editor)
|
||||
|
||||
if (attachedId < 0)
|
||||
attachNotification(id, notification)
|
||||
else
|
||||
Notifications.mgr.notify(id, notification.build())
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun notifyRemove(id: Int): Notification.Builder? {
|
||||
val idx = notifications.indexOfKey(id)
|
||||
var n: Notification.Builder? = null
|
||||
|
||||
if (idx >= 0) {
|
||||
n = notifications.valueAt(idx)
|
||||
notifications.removeAt(idx)
|
||||
|
||||
// The cancelled notification is the one attached to the session, need special handling
|
||||
if (attachedId == id) {
|
||||
if (notifications.isNotEmpty()) {
|
||||
// There are still remaining notifications, pick one and attach to the session
|
||||
val anotherId = notifications.keyAt(0)
|
||||
val notification = notifications.valueAt(0)
|
||||
attachNotification(anotherId, notification)
|
||||
} else {
|
||||
// No more notifications left, terminate the session
|
||||
attachedId = -1
|
||||
session.onDownloadComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.mgr.cancel(id)
|
||||
return n
|
||||
}
|
||||
|
||||
private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||
val external = subject.file.outputStream()
|
||||
|
||||
if (isRunningAsStub) {
|
||||
val updateApk = StubApk.update(context)
|
||||
try {
|
||||
// Download full APK to stub update path
|
||||
stream.copyAndClose(TeeOutputStream(external, updateApk.outputStream()))
|
||||
|
||||
// Also upgrade stub
|
||||
notifyUpdate(subject.notifyId) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setContentTitle(context.getString(R.string.hide_app_title))
|
||||
.setContentText("")
|
||||
}
|
||||
|
||||
// Extract stub
|
||||
val zf = ZipFile(updateApk)
|
||||
val apk = context.cachedFile("stub.apk")
|
||||
apk.delete()
|
||||
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||
zf.close()
|
||||
|
||||
// Patch and install
|
||||
subject.intent = HideAPK.upgrade(context, apk)
|
||||
?: throw IOException("HideAPK patch error")
|
||||
apk.delete()
|
||||
} catch (e: Exception) {
|
||||
// If any error occurred, do not let stub load the new APK
|
||||
updateApk.delete()
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
val session = APKInstall.startSession(context)
|
||||
stream.copyAndClose(TeeOutputStream(external, session.openStream(context)))
|
||||
subject.intent = session.waitIntent()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleModule(src: InputStream, file: Uri) {
|
||||
val input = ZipInputStream(src)
|
||||
val output = ZipOutputStream(file.outputStream())
|
||||
|
||||
withStreams(input, output) { zin, zout ->
|
||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||
context.assets.open("module_installer.sh").use { it.copyAll(zout) }
|
||||
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||
zout.write("#MAGISK\n".toByteArray())
|
||||
|
||||
zin.forEach { entry ->
|
||||
val path = entry.name
|
||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||
zout.putNextEntry(ZipEntry(path))
|
||||
if (!entry.isDirectory) {
|
||||
zin.copyAll(zout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TeeOutputStream(
|
||||
private val o1: OutputStream,
|
||||
private val o2: OutputStream
|
||||
) : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
o1.write(b)
|
||||
o2.write(b)
|
||||
}
|
||||
override fun write(b: ByteArray?, off: Int, len: Int) {
|
||||
o1.write(b, off, len)
|
||||
o2.write(b, off, len)
|
||||
}
|
||||
override fun close() {
|
||||
o1.close()
|
||||
o2.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
||||
val max = contentLength()
|
||||
val total = max.toFloat() / 1048576
|
||||
val id = subject.notifyId
|
||||
|
||||
notifyUpdate(id) { it.setContentTitle(subject.title) }
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it.toFloat() / 1048576
|
||||
notifyUpdate(id) { notification ->
|
||||
if (max > 0) {
|
||||
broadcast(progress / total, subject)
|
||||
notification
|
||||
.setProgress(max.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, total))
|
||||
} else {
|
||||
broadcast(-1f, subject)
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.*
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toFile
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.*
|
||||
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.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.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
class DownloadService : NotificationService() {
|
||||
|
||||
private val job = Job()
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { download(it) }
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun download(subject: Subject) {
|
||||
update(subject.notifyId)
|
||||
val coroutineScope = CoroutineScope(job + Dispatchers.IO)
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
||||
when (subject) {
|
||||
is Subject.App -> handleApp(stream, subject)
|
||||
is Subject.Module -> handleModule(stream, subject.file)
|
||||
}
|
||||
val activity = ActivityTracker.foreground
|
||||
if (activity != null && subject.autoLaunch) {
|
||||
remove(subject.notifyId)
|
||||
subject.pendingIntent(activity)?.send()
|
||||
} else {
|
||||
notifyFinish(subject)
|
||||
}
|
||||
subject.postDownload?.invoke()
|
||||
if (!hasNotifications)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
notifyFail(subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||
fun writeTee(output: OutputStream) {
|
||||
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||
val external = uri.outputStream()
|
||||
stream.copyAndClose(TeeOutputStream(external, output))
|
||||
}
|
||||
|
||||
if (isRunningAsStub) {
|
||||
val updateApk = StubApk.update(this)
|
||||
try {
|
||||
// Download full APK to stub update path
|
||||
writeTee(updateApk.outputStream())
|
||||
|
||||
if (Info.stub!!.version < subject.stub.versionCode) {
|
||||
// Also upgrade stub
|
||||
update(subject.notifyId) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_app_title))
|
||||
.setContentText("")
|
||||
}
|
||||
|
||||
// Download
|
||||
val apk = subject.file.toFile()
|
||||
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
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.postDownload = { Runtime.getRuntime().exit(0) }
|
||||
}
|
||||
return
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// If any error occurred, do not let stub load the new APK
|
||||
updateApk.delete()
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
val session = APKInstall.startSession(this)
|
||||
writeTee(session.openStream(this))
|
||||
subject.intent = session.waitIntent()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleModule(src: InputStream, file: Uri) {
|
||||
val input = ZipInputStream(src.buffered())
|
||||
val output = ZipOutputStream(file.outputStream().buffered())
|
||||
|
||||
withStreams(input, output) { zin, zout ->
|
||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||
assets.open("module_installer.sh").copyTo(zout)
|
||||
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||
zout.write("#MAGISK\n".toByteArray())
|
||||
|
||||
zin.forEach { entry ->
|
||||
val path = entry.name
|
||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||
zout.putNextEntry(ZipEntry(path))
|
||||
if (!entry.isDirectory) {
|
||||
zin.copyTo(zout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TeeOutputStream(
|
||||
private val o1: OutputStream,
|
||||
private val o2: OutputStream
|
||||
) : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
o1.write(b)
|
||||
o2.write(b)
|
||||
}
|
||||
override fun write(b: ByteArray?, off: Int, len: Int) {
|
||||
o1.write(b, off, len)
|
||||
o2.write(b, off, len)
|
||||
}
|
||||
override fun close() {
|
||||
o1.close()
|
||||
o2.close()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SUBJECT_KEY = "subject"
|
||||
private const val REQUEST_CODE = 1
|
||||
|
||||
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
||||
progressBroadcast.value = null
|
||||
progressBroadcast.observe(owner) {
|
||||
val (progress, subject) = it ?: return@observe
|
||||
callback(progress, subject)
|
||||
}
|
||||
}
|
||||
|
||||
private fun intent(context: Context, subject: Subject) =
|
||||
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
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) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ktx.synchronized
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.InputStream
|
||||
|
||||
open class NotificationService : BaseService() {
|
||||
|
||||
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
|
||||
protected val hasNotifications get() = notifications.isNotEmpty()
|
||||
|
||||
protected val service get() = ServiceLocator.networkService
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
notifications.forEach { Notifications.mgr.cancel(it.key) }
|
||||
notifications.clear()
|
||||
}
|
||||
|
||||
protected fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
||||
val max = contentLength()
|
||||
val total = max.toFloat() / 1048576
|
||||
val id = subject.notifyId
|
||||
|
||||
update(id) { it.setContentTitle(subject.title) }
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it.toFloat() / 1048576
|
||||
update(id) { notification ->
|
||||
if (max > 0) {
|
||||
broadcast(progress / total, subject)
|
||||
notification
|
||||
.setProgress(max.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, total))
|
||||
} else {
|
||||
broadcast(-1f, subject)
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||
val notification = remove(id)?.also(editor) ?: return -1
|
||||
val newId = Notifications.nextId()
|
||||
Notifications.mgr.notify(newId, notification.build())
|
||||
return newId
|
||||
}
|
||||
|
||||
protected fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
|
||||
broadcast(-2f, subject)
|
||||
it.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
|
||||
protected fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
|
||||
broadcast(1f, subject)
|
||||
it.setContentTitle(subject.title)
|
||||
.setContentText(getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
|
||||
}
|
||||
|
||||
private fun create() = Notifications.progress(this, "")
|
||||
|
||||
private fun updateForeground() {
|
||||
if (hasNotifications) {
|
||||
val (id, notification) = notifications.entries.first()
|
||||
startForeground(id, notification.build())
|
||||
} else {
|
||||
stopForeground(false)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
val wasEmpty = !hasNotifications
|
||||
val notification = notifications.getOrPut(id, ::create).also(editor)
|
||||
if (wasEmpty)
|
||||
updateForeground()
|
||||
else
|
||||
Notifications.mgr.notify(id, notification.build())
|
||||
}
|
||||
|
||||
protected fun remove(id: Int): Notification.Builder? {
|
||||
val n = notifications.remove(id)?.also { updateForeground() }
|
||||
Notifications.mgr.cancel(id)
|
||||
return n
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
protected val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
||||
|
||||
private fun broadcast(progress: Float, subject: Subject) {
|
||||
progressBroadcast.postValue(progress to subject)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,23 +8,17 @@ import android.net.Uri
|
|||
import android.os.Parcelable
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||
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.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri()
|
||||
|
||||
enum class Action {
|
||||
Flash,
|
||||
Download
|
||||
}
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
sealed class Subject : Parcelable {
|
||||
|
||||
|
@ -33,19 +27,17 @@ sealed class Subject : Parcelable {
|
|||
abstract val title: String
|
||||
abstract val notifyId: Int
|
||||
open val autoLaunch: Boolean get() = true
|
||||
open val postDownload: (() -> Unit)? get() = null
|
||||
|
||||
abstract fun pendingIntent(context: Context): PendingIntent?
|
||||
open fun pendingIntent(context: Context): PendingIntent? = null
|
||||
|
||||
@Parcelize
|
||||
class Module(
|
||||
val module: OnlineModule,
|
||||
val action: Action,
|
||||
private val module: OnlineModule,
|
||||
override val autoLaunch: Boolean,
|
||||
override val notifyId: Int = Notifications.nextId()
|
||||
) : Subject() {
|
||||
override val url: String get() = module.zipUrl
|
||||
override val title: String get() = module.downloadFilename
|
||||
override val autoLaunch: Boolean get() = action == Action.Flash
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
|
@ -59,7 +51,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})"
|
||||
|
@ -67,17 +58,24 @@ sealed class Subject : Parcelable {
|
|||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
cachedFile("manager.apk")
|
||||
MediaStoreUtils.getFile("${title}.apk").uri
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
override var postDownload: (() -> Unit)? = null
|
||||
|
||||
@IgnoredOnParcel
|
||||
var intent: Intent? = null
|
||||
override fun pendingIntent(context: Context) = intent?.toPending(context)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class Test(
|
||||
override val notifyId: Int = Notifications.nextId(),
|
||||
override val title: String = UUID.randomUUID().toString().substring(0, 6)
|
||||
) : Subject() {
|
||||
override val url get() = "https://link.testfile.org/250MB"
|
||||
override val file get() = File("/dev/null").toUri()
|
||||
override val autoLaunch get() = false
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
protected fun Intent.toPending(context: Context): PendingIntent {
|
||||
return PendingIntent.getActivity(context, notifyId, this,
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
package com.topjohnwu.magisk.core.ktx
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Process
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import java.io.File
|
||||
import kotlin.String
|
||||
|
||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||
|
||||
fun Context.getBitmap(id: Int): Bitmap {
|
||||
var drawable = AppCompatResources.getDrawable(this, id)!!
|
||||
if (drawable is BitmapDrawable)
|
||||
return drawable.bitmap
|
||||
if (SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) {
|
||||
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth, drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
drawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
val Context.deviceProtectedContext: Context get() =
|
||||
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||
createDeviceProtectedStorageContext()
|
||||
} else { this }
|
||||
|
||||
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
||||
|
||||
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||
runCatching {
|
||||
if (labelRes > 0) {
|
||||
val res = pm.getResourcesForApplication(this)
|
||||
val config = Configuration()
|
||||
config.setLocale(currentLocale)
|
||||
res.updateConfiguration(config, res.displayMetrics)
|
||||
return res.getString(labelRes)
|
||||
}
|
||||
}
|
||||
|
||||
return loadLabel(pm).toString()
|
||||
}
|
||||
|
||||
fun Context.unwrap(): Context {
|
||||
var context = this
|
||||
while (context is ContextWrapper)
|
||||
context = context.baseContext
|
||||
return context
|
||||
}
|
||||
|
||||
fun Activity.hideKeyboard() {
|
||||
val view = currentFocus ?: return
|
||||
getSystemService<InputMethodManager>()
|
||||
?.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
view.clearFocus()
|
||||
}
|
||||
|
||||
val View.activity: Activity get() {
|
||||
var context = context
|
||||
while(true) {
|
||||
if (context !is ContextWrapper)
|
||||
error("View is not attached to activity")
|
||||
if (context is Activity)
|
||||
return context
|
||||
context = context.baseContext
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
fun getProperty(key: String, def: String): String {
|
||||
runCatching {
|
||||
val clazz = Class.forName("android.os.SystemProperties")
|
||||
val get = clazz.getMethod("get", String::class.java, String::class.java)
|
||||
return get.invoke(clazz, key, def) as String
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
|
||||
val flag = PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
val pkgs = getPackagesForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||
if (pkgs.size > 1) {
|
||||
if (pid <= 0) {
|
||||
return null
|
||||
}
|
||||
// Try to find package name from PID
|
||||
val proc = RootUtils.obj?.getAppProcess(pid)
|
||||
if (proc == null) {
|
||||
if (uid == Process.SHELL_UID) {
|
||||
// It is possible that some apps installed are sharing UID with shell.
|
||||
// We will not be able to find a package from the active process list,
|
||||
// because the client is forked from ADB shell, not any app process.
|
||||
return getPackageInfo("com.android.shell", flag)
|
||||
}
|
||||
} else if (uid == proc.uid) {
|
||||
return getPackageInfo(proc.pkgList[0], flag)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
if (pkgs.size == 1) {
|
||||
return getPackageInfo(pkgs[0], flag)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
fun Context.toast(msg: CharSequence, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(this, msg, duration).show() }
|
||||
}
|
||||
|
||||
fun Context.toast(resId: Int, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(this, resId, duration).show() }
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package com.topjohnwu.magisk.core.ktx
|
||||
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Collections
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
inline fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||
var entry: ZipEntry? = nextEntry
|
||||
while (entry != null) {
|
||||
callback(entry)
|
||||
entry = nextEntry
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||
inStream: In,
|
||||
outStream: Out,
|
||||
withBoth: (In, Out) -> Unit
|
||||
) {
|
||||
inStream.use { reader ->
|
||||
outStream.use { writer ->
|
||||
withBoth(reader, writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
suspend fun InputStream.copyAll(
|
||||
out: OutputStream,
|
||||
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
): Long {
|
||||
return withContext(dispatcher) {
|
||||
var bytesCopied: Long = 0
|
||||
val buffer = ByteArray(bufferSize)
|
||||
var bytes = read(buffer)
|
||||
while (isActive && bytes >= 0) {
|
||||
out.write(buffer, 0, bytes)
|
||||
bytesCopied += bytes
|
||||
bytes = read(buffer)
|
||||
}
|
||||
bytesCopied
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
suspend inline fun InputStream.copyAndClose(
|
||||
out: OutputStream,
|
||||
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) = withStreams(this, out) { i, o -> i.copyAll(o, bufferSize, dispatcher) }
|
||||
|
||||
@Throws(IOException::class)
|
||||
suspend inline fun InputStream.writeTo(
|
||||
file: File,
|
||||
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) = copyAndClose(file.outputStream(), bufferSize, dispatcher)
|
||||
|
||||
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
|
||||
put(key, value)
|
||||
}
|
||||
|
||||
fun <T> MutableList<T>.synchronized(): MutableList<T> = Collections.synchronizedList(this)
|
||||
|
||||
fun <T> MutableSet<T>.synchronized(): MutableSet<T> = Collections.synchronizedSet(this)
|
||||
|
||||
fun <K, V> MutableMap<K, V>.synchronized(): MutableMap<K, V> = Collections.synchronizedMap(this)
|
||||
|
||||
fun Class<*>.reflectField(name: String): Field =
|
||||
getDeclaredField(name).apply { isAccessible = true }
|
||||
|
||||
inline fun <T, R> Flow<T>.concurrentMap(crossinline transform: suspend (T) -> R): Flow<R> {
|
||||
return flatMapMerge { value ->
|
||||
flow { emit(transform(value)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun Long.toTime(format: DateFormat) = format.format(this).orEmpty()
|
||||
|
||||
// Some devices don't allow filenames containing ":"
|
||||
val timeFormatStandard by lazy {
|
||||
SimpleDateFormat(
|
||||
"yyyy-MM-dd'T'HH.mm.ss",
|
||||
currentLocale
|
||||
)
|
||||
}
|
||||
val timeDateFormat: DateFormat by lazy {
|
||||
DateFormat.getDateTimeInstance(
|
||||
DateFormat.DEFAULT,
|
||||
DateFormat.DEFAULT,
|
||||
currentLocale
|
||||
)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.topjohnwu.magisk.core.ktx
|
||||
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
fun reboot(reason: String = if (Config.recovery) "recovery" else "") {
|
||||
if (reason == "recovery") {
|
||||
// KEYCODE_POWER = 26, hide incorrect "Factory data reset" message
|
||||
Shell.cmd("/system/bin/input keyevent 26").submit()
|
||||
}
|
||||
Shell.cmd("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
||||
}
|
||||
|
||||
suspend fun Shell.Job.await() = withContext(Dispatchers.IO) { exec() }
|
|
@ -1,28 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
|
||||
abstract class BaseDao {
|
||||
|
||||
object Table {
|
||||
const val POLICY = "policies"
|
||||
const val LOG = "logs"
|
||||
const val SETTINGS = "settings"
|
||||
const val STRINGS = "strings"
|
||||
}
|
||||
|
||||
@StringDef(Table.POLICY, Table.LOG, Table.SETTINGS, Table.STRINGS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class TableStrict
|
||||
|
||||
@TableStrict
|
||||
abstract val table: String
|
||||
|
||||
inline fun <reified Builder : Query.Builder> buildQuery(builder: Builder.() -> Unit = {}) =
|
||||
Builder::class.java.newInstance()
|
||||
.apply { table = this@BaseDao.table }
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let { Query(it) }
|
||||
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class PolicyDao : BaseDao() {
|
||||
|
||||
override val table: String = Table.POLICY
|
||||
|
||||
suspend fun deleteOutdated() = buildQuery<Delete> {
|
||||
condition {
|
||||
greaterThan("until", "0")
|
||||
and {
|
||||
lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString())
|
||||
}
|
||||
or {
|
||||
lessThan("until", "0")
|
||||
}
|
||||
}
|
||||
}.commit()
|
||||
|
||||
suspend fun delete(uid: Int) = buildQuery<Delete> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.commit()
|
||||
|
||||
suspend fun fetch(uid: Int) = buildQuery<Select> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.query().first().toPolicyOrNull()
|
||||
|
||||
suspend fun update(policy: SuPolicy) = buildQuery<Replace> {
|
||||
values(policy.toMap())
|
||||
}.commit()
|
||||
|
||||
suspend fun <R: Any> fetchAll(mapper: (SuPolicy) -> R) = buildQuery<Select> {
|
||||
condition {
|
||||
equals("uid/100000", Const.USER_ID)
|
||||
}
|
||||
}.query {
|
||||
it.toPolicyOrNull()?.let(mapper)
|
||||
}
|
||||
|
||||
private fun Map<String, String>.toPolicyOrNull(): SuPolicy? {
|
||||
return runCatching { toPolicy(AppContext.packageManager) }.getOrElse {
|
||||
Timber.w(it)
|
||||
val uid = getOrElse("uid") { return null }
|
||||
GlobalScope.launch { delete(uid.toInt()) }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class Query(private val _query: String) {
|
||||
val query get() = "magisk --sqlite '$_query'"
|
||||
|
||||
interface Builder {
|
||||
val requestType: String
|
||||
var table: String
|
||||
}
|
||||
|
||||
suspend inline fun <R : Any> query(crossinline mapper: (Map<String, String>) -> R?): List<R> =
|
||||
withContext(Dispatchers.Default) {
|
||||
Shell.cmd(query).await().out.map { line ->
|
||||
async {
|
||||
line.split("\\|".toRegex())
|
||||
.map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.map { it[0] to it[1] }
|
||||
.toMap()
|
||||
.let(mapper)
|
||||
}
|
||||
}.awaitAll().filterNotNull()
|
||||
}
|
||||
|
||||
suspend inline fun query() = query { it }
|
||||
|
||||
suspend inline fun commit() = Shell.cmd(query).to(null).await()
|
||||
}
|
||||
|
||||
class Delete : Query.Builder {
|
||||
override val requestType: String = "DELETE FROM"
|
||||
override var table = ""
|
||||
|
||||
private var condition = ""
|
||||
|
||||
fun condition(builder: Condition.() -> Unit) {
|
||||
condition = Condition().apply(builder).toString()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, condition).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Select : Query.Builder {
|
||||
override val requestType: String get() = "SELECT $fields FROM"
|
||||
override lateinit var table: String
|
||||
|
||||
private var fields = "*"
|
||||
private var condition = ""
|
||||
private var orderField = ""
|
||||
|
||||
fun fields(vararg newFields: String) {
|
||||
if (newFields.isEmpty()) {
|
||||
fields = "*"
|
||||
return
|
||||
}
|
||||
fields = newFields.joinToString(", ")
|
||||
}
|
||||
|
||||
fun condition(builder: Condition.() -> Unit) {
|
||||
condition = Condition().apply(builder).toString()
|
||||
}
|
||||
|
||||
fun orderBy(field: String, @OrderStrict order: String) {
|
||||
orderField = "ORDER BY $field $order"
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, condition, orderField).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Replace : Insert() {
|
||||
override val requestType: String = "REPLACE INTO"
|
||||
}
|
||||
|
||||
open class Insert : Query.Builder {
|
||||
override val requestType: String = "INSERT INTO"
|
||||
override lateinit var table: String
|
||||
|
||||
private val keys get() = _values.keys.joinToString(",")
|
||||
private val values get() = _values.values.joinToString(",") {
|
||||
when (it) {
|
||||
is Boolean -> if (it) "1" else "0"
|
||||
is Number -> it.toString()
|
||||
else -> "\"$it\""
|
||||
}
|
||||
}
|
||||
private var _values: Map<String, Any> = mapOf()
|
||||
|
||||
fun values(vararg pairs: Pair<String, Any>) {
|
||||
_values = pairs.toMap()
|
||||
}
|
||||
|
||||
fun values(values: Map<String, Any>) {
|
||||
_values = values
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Condition {
|
||||
|
||||
private val conditionWord = "WHERE %s"
|
||||
private var condition: String = ""
|
||||
|
||||
fun equals(field: String, value: Any) {
|
||||
condition = when (value) {
|
||||
is String -> "$field=\"$value\""
|
||||
else -> "$field=$value"
|
||||
}
|
||||
}
|
||||
|
||||
fun greaterThan(field: String, value: String) {
|
||||
condition = "$field > $value"
|
||||
}
|
||||
|
||||
fun lessThan(field: String, value: String) {
|
||||
condition = "$field < $value"
|
||||
}
|
||||
|
||||
fun greaterOrEqualTo(field: String, value: String) {
|
||||
condition = "$field >= $value"
|
||||
}
|
||||
|
||||
fun lessOrEqualTo(field: String, value: String) {
|
||||
condition = "$field <= $value"
|
||||
}
|
||||
|
||||
fun and(builder: Condition.() -> Unit) {
|
||||
condition = "($condition AND ${Condition().apply(builder).condition})"
|
||||
}
|
||||
|
||||
fun or(builder: Condition.() -> Unit) {
|
||||
condition = "($condition OR ${Condition().apply(builder).condition})"
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return conditionWord.format(condition)
|
||||
}
|
||||
}
|
||||
|
||||
object Order {
|
||||
const val ASC = "ASC"
|
||||
const val DESC = "DESC"
|
||||
}
|
||||
|
||||
@StringDef(Order.ASC, Order.DESC)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class OrderStrict
|
|
@ -1,22 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class SettingsDao : BaseDao() {
|
||||
|
||||
override val table = Table.SETTINGS
|
||||
|
||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.commit()
|
||||
|
||||
suspend fun put(key: String, value: Int) = buildQuery<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.commit()
|
||||
|
||||
suspend fun fetch(key: String, default: Int = -1) = buildQuery<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.query {
|
||||
it["value"]?.toIntOrNull()
|
||||
}.firstOrNull() ?: default
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class StringDao : BaseDao() {
|
||||
|
||||
override val table = Table.STRINGS
|
||||
|
||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.commit()
|
||||
|
||||
suspend fun put(key: String, value: String) = buildQuery<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.commit()
|
||||
|
||||
suspend fun fetch(key: String, default: String = "") = buildQuery<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.query {
|
||||
it["value"]
|
||||
}.firstOrNull() ?: default
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -2,9 +2,9 @@ package com.topjohnwu.magisk.core.model.module
|
|||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
@ -26,13 +26,12 @@ data class LocalModule(
|
|||
var outdated = false
|
||||
|
||||
private var updateUrl: String = ""
|
||||
private val removeFile = SuFile(path, "remove")
|
||||
private val disableFile = SuFile(path, "disable")
|
||||
private val updateFile = SuFile(path, "update")
|
||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
||||
private val riruFolder = SuFile(path, "riru")
|
||||
private val zygiskFolder = SuFile(path, "zygisk")
|
||||
private val unloaded = SuFile(zygiskFolder, "unloaded")
|
||||
private val removeFile = RootUtils.fs.getFile(path, "remove")
|
||||
private val disableFile = RootUtils.fs.getFile(path, "disable")
|
||||
private val updateFile = RootUtils.fs.getFile(path, "update")
|
||||
private val riruFolder = RootUtils.fs.getFile(path, "riru")
|
||||
private val zygiskFolder = RootUtils.fs.getFile(path, "zygisk")
|
||||
private val unloaded = RootUtils.fs.getFile(zygiskFolder, "unloaded")
|
||||
|
||||
val updated: Boolean get() = updateFile.exists()
|
||||
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
||||
|
@ -42,19 +41,12 @@ data class LocalModule(
|
|||
var enable: Boolean
|
||||
get() = !disableFile.exists()
|
||||
set(enable) {
|
||||
val dir = "$PERSIST/$id"
|
||||
if (enable) {
|
||||
disableFile.delete()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.cmd("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
} else {
|
||||
!disableFile.createNewFile()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.cmd("rm -rf $dir").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,16 +56,10 @@ data class LocalModule(
|
|||
if (remove) {
|
||||
if (updateFile.exists()) return
|
||||
removeFile.createNewFile()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.cmd("rm -rf $PERSIST/$id").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
} else {
|
||||
removeFile.delete()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.cmd("cp -af $ruleFile $PERSIST/$id").submit()
|
||||
Shell.cmd("copy_preinit_files").submit()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,13 +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) {
|
||||
SuFile(Const.MAGISK_PATH)
|
||||
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) }
|
||||
}
|
||||
|
|
|
@ -1,19 +1,73 @@
|
|||
package com.topjohnwu.magisk.core.model.su
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
|
||||
@Entity(tableName = "logs")
|
||||
data class SuLog(
|
||||
class SuLog(
|
||||
val fromUid: Int,
|
||||
val toUid: Int,
|
||||
val fromPid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val command: String,
|
||||
val action: Boolean,
|
||||
val time: Long = now
|
||||
val action: Int,
|
||||
val target: Int,
|
||||
val context: String,
|
||||
val gids: String,
|
||||
val time: Long = System.currentTimeMillis()
|
||||
) {
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||
}
|
||||
|
||||
fun PackageManager.createSuLog(
|
||||
info: PackageInfo,
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String,
|
||||
policy: Int,
|
||||
target: Int,
|
||||
context: String,
|
||||
gids: String,
|
||||
): SuLog {
|
||||
val appInfo = info.applicationInfo
|
||||
return SuLog(
|
||||
fromUid = appInfo.uid,
|
||||
toUid = toUid,
|
||||
fromPid = fromPid,
|
||||
packageName = getNameForUid(appInfo.uid)!!,
|
||||
appName = appInfo.getLabel(this),
|
||||
command = command,
|
||||
action = policy,
|
||||
target = target,
|
||||
context = context,
|
||||
gids = gids,
|
||||
)
|
||||
}
|
||||
|
||||
fun createSuLog(
|
||||
fromUid: Int,
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String,
|
||||
policy: Int,
|
||||
target: Int,
|
||||
context: String,
|
||||
gids: String,
|
||||
): SuLog {
|
||||
return SuLog(
|
||||
fromUid = fromUid,
|
||||
toUid = toUid,
|
||||
fromPid = fromPid,
|
||||
packageName = "[UID] $fromUid",
|
||||
appName = "[UID] $fromUid",
|
||||
command = command,
|
||||
action = policy,
|
||||
target = target,
|
||||
context = context,
|
||||
gids = gids,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,85 +1,22 @@
|
|||
@file:SuppressLint("InlinedApi")
|
||||
|
||||
package com.topjohnwu.magisk.core.model.su
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
|
||||
data class SuPolicy(
|
||||
val uid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val icon: Drawable,
|
||||
var policy: Int = INTERACTIVE,
|
||||
var until: Long = -1L,
|
||||
val logging: Boolean = true,
|
||||
val notification: Boolean = true
|
||||
) {
|
||||
|
||||
class SuPolicy(val uid: Int) {
|
||||
companion object {
|
||||
const val INTERACTIVE = 0
|
||||
const val DENY = 1
|
||||
const val ALLOW = 2
|
||||
}
|
||||
|
||||
fun toLog(toUid: Int, fromPid: Int, command: String) = SuLog(
|
||||
uid, toUid, fromPid, packageName, appName,
|
||||
command, policy == ALLOW)
|
||||
var policy: Int = INTERACTIVE
|
||||
var until: Long = -1L
|
||||
var logging: Boolean = true
|
||||
var notification: Boolean = true
|
||||
|
||||
fun toMap() = mapOf(
|
||||
fun toMap(): MutableMap<String, Any> = mutableMapOf(
|
||||
"uid" to uid,
|
||||
"package_name" to packageName,
|
||||
"policy" to policy,
|
||||
"until" to until,
|
||||
"logging" to logging,
|
||||
"notification" to notification
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
|
||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
||||
val packageName = get("package_name").orEmpty()
|
||||
val info = pm.getApplicationInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES)
|
||||
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
|
||||
return SuPolicy(
|
||||
uid = uid,
|
||||
packageName = packageName,
|
||||
appName = info.getLabel(pm),
|
||||
icon = info.loadIcon(pm),
|
||||
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
|
||||
until = get("until")?.toLongOrNull() ?: -1L,
|
||||
logging = get("logging")?.toIntOrNull() != 0,
|
||||
notification = get("notification")?.toIntOrNull() != 0
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
|
||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
||||
?: throw PackageManager.NameNotFoundException()
|
||||
val info = pm.getApplicationInfo(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES)
|
||||
return SuPolicy(
|
||||
uid = info.uid,
|
||||
packageName = pkg,
|
||||
appName = info.getLabel(pm),
|
||||
icon = info.loadIcon(pm),
|
||||
policy = policy
|
||||
)
|
||||
}
|
||||
|
||||
fun Int.toUidPolicy(pm: PackageManager, policy: Int): SuPolicy {
|
||||
return SuPolicy(
|
||||
uid = this,
|
||||
packageName = "[UID] $this",
|
||||
appName = "[UID] $this",
|
||||
icon = pm.defaultActivityIcon,
|
||||
policy = policy
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package com.topjohnwu.magisk.data.repository
|
||||
package com.topjohnwu.magisk.core.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
|
@ -11,26 +11,27 @@ import kotlin.reflect.KProperty
|
|||
interface DBConfig {
|
||||
val settingsDB: SettingsDao
|
||||
val stringDB: StringDao
|
||||
val coroutineScope: CoroutineScope
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Int
|
||||
) = DBSettingsValue(name, default)
|
||||
) = IntDBProperty(name, default)
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) = DBBoolSettings(name, default)
|
||||
) = BoolDBProperty(name, default)
|
||||
|
||||
fun dbStrings(
|
||||
name: String,
|
||||
default: String,
|
||||
sync: Boolean = false
|
||||
) = DBStringsValue(name, default, sync)
|
||||
) = StringDBProperty(name, default, sync)
|
||||
|
||||
}
|
||||
|
||||
class DBSettingsValue(
|
||||
class IntDBProperty(
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
) : ReadWriteProperty<DBConfig, Int> {
|
||||
|
@ -48,18 +49,18 @@ class DBSettingsValue(
|
|||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
GlobalScope.launch {
|
||||
thisRef.coroutineScope.launch {
|
||||
thisRef.settingsDB.put(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class DBBoolSettings(
|
||||
open class BoolDBProperty(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) : ReadWriteProperty<DBConfig, Boolean> {
|
||||
|
||||
val base = DBSettingsValue(name, if (default) 1 else 0)
|
||||
val base = IntDBProperty(name, if (default) 1 else 0)
|
||||
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
||||
base.getValue(thisRef, property) != 0
|
||||
|
@ -68,10 +69,10 @@ open class DBBoolSettings(
|
|||
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||
}
|
||||
|
||||
class DBBoolSettingsNoWrite(
|
||||
class BoolDBPropertyNoWrite(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) : DBBoolSettings(name, default) {
|
||||
) : BoolDBProperty(name, default) {
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
|
||||
synchronized(base) {
|
||||
base.value = if (value) 1 else 0
|
||||
|
@ -79,7 +80,7 @@ class DBBoolSettingsNoWrite(
|
|||
}
|
||||
}
|
||||
|
||||
class DBStringsValue(
|
||||
class StringDBProperty(
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val sync: Boolean
|
||||
|
@ -106,7 +107,7 @@ class DBStringsValue(
|
|||
thisRef.stringDB.delete(name)
|
||||
}
|
||||
} else {
|
||||
GlobalScope.launch {
|
||||
thisRef.coroutineScope.launch {
|
||||
thisRef.stringDB.delete(name)
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +117,7 @@ class DBStringsValue(
|
|||
thisRef.stringDB.put(name, value)
|
||||
}
|
||||
} else {
|
||||
GlobalScope.launch {
|
||||
thisRef.coroutineScope.launch {
|
||||
thisRef.stringDB.put(name, value)
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
package com.topjohnwu.magisk.data.repository
|
||||
package com.topjohnwu.magisk.core.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.data.SuLogDao
|
||||
import com.topjohnwu.magisk.core.ktx.await
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import com.topjohnwu.magisk.data.database.SuLogDao
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
|
|
@ -1,35 +1,34 @@
|
|||
package com.topjohnwu.magisk.data.repository
|
||||
package com.topjohnwu.magisk.core.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.DEBUG_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubPageServices
|
||||
import com.topjohnwu.magisk.data.network.RawServices
|
||||
import com.topjohnwu.magisk.core.data.GithubPageServices
|
||||
import com.topjohnwu.magisk.core.data.RawServices
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
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) {
|
||||
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
||||
BETA_CHANNEL -> fetchBetaUpdate()
|
||||
CANARY_CHANNEL -> fetchCanaryUpdate()
|
||||
DEBUG_CHANNEL -> fetchDebugUpdate()
|
||||
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
if (info.magisk.versionCode < Info.env.versionCode &&
|
||||
Config.updateChannel == DEFAULT_CHANNEL
|
||||
) {
|
||||
Config.updateChannel == DEFAULT_CHANNEL) {
|
||||
Config.updateChannel = BETA_CHANNEL
|
||||
info = fetchBetaUpdate()
|
||||
}
|
||||
|
@ -40,11 +39,15 @@ class NetworkService(
|
|||
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
||||
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
||||
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
||||
private suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
|
||||
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
|
||||
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
|
|
@ -1,11 +1,73 @@
|
|||
package com.topjohnwu.magisk.data.preference
|
||||
package com.topjohnwu.magisk.core.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
abstract class Property {
|
||||
interface PreferenceConfig {
|
||||
|
||||
val context: Context
|
||||
|
||||
val fileName: String
|
||||
get() = "${context.packageName}_preferences"
|
||||
|
||||
val prefs: SharedPreferences
|
||||
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||
|
||||
fun preferenceStrInt(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = false
|
||||
) = object: ReadWriteProperty<PreferenceConfig, Int> {
|
||||
val base = StringProperty(name, default.toString(), commit)
|
||||
override fun getValue(thisRef: PreferenceConfig, property: KProperty<*>): Int =
|
||||
base.getValue(thisRef, property).toInt()
|
||||
|
||||
override fun setValue(thisRef: PreferenceConfig, property: KProperty<*>, value: Int) =
|
||||
base.setValue(thisRef, property, value.toString())
|
||||
}
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Boolean,
|
||||
commit: Boolean = false
|
||||
) = BooleanProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Float,
|
||||
commit: Boolean = false
|
||||
) = FloatProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = false
|
||||
) = IntProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Long,
|
||||
commit: Boolean = false
|
||||
) = LongProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: String,
|
||||
commit: Boolean = false
|
||||
) = StringProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Set<String>,
|
||||
commit: Boolean = false
|
||||
) = StringSetProperty(name, default, commit)
|
||||
|
||||
}
|
||||
|
||||
abstract class PreferenceProperty {
|
||||
|
||||
fun SharedPreferences.Editor.put(name: String, value: Boolean) = putBoolean(name, value)
|
||||
fun SharedPreferences.Editor.put(name: String, value: Float) = putFloat(name, value)
|
||||
|
@ -27,10 +89,10 @@ class BooleanProperty(
|
|||
private val name: String,
|
||||
private val default: Boolean,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Boolean> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Boolean> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Boolean {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
|
@ -38,7 +100,7 @@ class BooleanProperty(
|
|||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Boolean
|
||||
) {
|
||||
|
@ -51,10 +113,10 @@ class FloatProperty(
|
|||
private val name: String,
|
||||
private val default: Float,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Float> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Float> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Float {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
|
@ -62,7 +124,7 @@ class FloatProperty(
|
|||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Float
|
||||
) {
|
||||
|
@ -75,10 +137,10 @@ class IntProperty(
|
|||
private val name: String,
|
||||
private val default: Int,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Int> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Int> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Int {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
|
@ -86,7 +148,7 @@ class IntProperty(
|
|||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Int
|
||||
) {
|
||||
|
@ -99,10 +161,10 @@ class LongProperty(
|
|||
private val name: String,
|
||||
private val default: Long,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Long> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Long> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Long {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
|
@ -110,7 +172,7 @@ class LongProperty(
|
|||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Long
|
||||
) {
|
||||
|
@ -123,10 +185,10 @@ class StringProperty(
|
|||
private val name: String,
|
||||
private val default: String,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, String> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, String> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): String {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
|
@ -134,7 +196,7 @@ class StringProperty(
|
|||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: String
|
||||
) {
|
||||
|
@ -147,10 +209,10 @@ class StringSetProperty(
|
|||
private val name: String,
|
||||
private val default: Set<String>,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Set<String>> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Set<String>> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Set<String> {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
|
@ -158,7 +220,7 @@ class StringSetProperty(
|
|||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Set<String>
|
||||
) {
|
|
@ -6,13 +6,13 @@ import android.widget.Toast
|
|||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toUidPolicy
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
|
||||
object SuCallbackHandler {
|
||||
|
@ -53,59 +53,50 @@ object SuCallbackHandler {
|
|||
private fun handleLogging(context: Context, data: Bundle) {
|
||||
val fromUid = data.getIntComp("from.uid", -1)
|
||||
val notify = data.getBoolean("notify", true)
|
||||
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||
val toUid = data.getIntComp("to.uid", -1)
|
||||
val pid = data.getIntComp("pid", -1)
|
||||
val command = data.getString("command", "")
|
||||
val target = data.getIntComp("target", -1)
|
||||
val seContext = data.getString("context", "")
|
||||
val gids = data.getString("gids", "")
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val policy = runCatching {
|
||||
fromUid.toPolicy(pm, allow)
|
||||
}.getOrElse {
|
||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
||||
fromUid.toUidPolicy(pm, allow)
|
||||
}
|
||||
val log = runCatching {
|
||||
pm.getPackageInfo(fromUid, pid)?.let {
|
||||
pm.createSuLog(it, toUid, pid, command, policy, target, seContext, gids)
|
||||
}
|
||||
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy, target, seContext, gids)
|
||||
|
||||
if (notify)
|
||||
notify(context, policy)
|
||||
notify(context, log.action == SuPolicy.ALLOW, log.appName)
|
||||
|
||||
val toUid = data.getIntComp("to.uid", -1)
|
||||
val pid = data.getIntComp("pid", -1)
|
||||
|
||||
val command = data.getString("command", "")
|
||||
val log = policy.toLog(
|
||||
toUid = toUid,
|
||||
fromPid = pid,
|
||||
command = command
|
||||
)
|
||||
|
||||
GlobalScope.launch {
|
||||
ServiceLocator.logRepo.insert(log)
|
||||
}
|
||||
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||
}
|
||||
|
||||
private fun handleNotify(context: Context, data: Bundle) {
|
||||
val fromUid = data.getIntComp("from.uid", -1)
|
||||
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||
val uid = data.getIntComp("from.uid", -1)
|
||||
val pid = data.getIntComp("pid", -1)
|
||||
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val appName = runCatching {
|
||||
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
|
||||
}.getOrNull() ?: "[UID] $uid"
|
||||
|
||||
val policy = runCatching {
|
||||
fromUid.toPolicy(pm, allow)
|
||||
}.getOrElse {
|
||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
||||
fromUid.toUidPolicy(pm, allow)
|
||||
}
|
||||
notify(context, policy)
|
||||
notify(context, policy == SuPolicy.ALLOW, appName)
|
||||
}
|
||||
|
||||
private fun notify(context: Context, policy: SuPolicy) {
|
||||
private fun notify(context: Context, granted: Boolean, appName: String) {
|
||||
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||
val resId = if (policy.policy == SuPolicy.ALLOW)
|
||||
val resId = if (granted)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
R.string.su_deny_toast
|
||||
|
||||
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
|
||||
context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,31 @@
|
|||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.Closeable
|
||||
import java.io.DataOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SuRequestHandler(
|
||||
private val pm: PackageManager,
|
||||
val pm: PackageManager,
|
||||
private val policyDB: PolicyDao
|
||||
) : Closeable {
|
||||
) {
|
||||
|
||||
private lateinit var output: DataOutputStream
|
||||
lateinit var policy: SuPolicy
|
||||
private lateinit var output: File
|
||||
private lateinit var policy: SuPolicy
|
||||
lateinit var pkgInfo: PackageInfo
|
||||
private set
|
||||
|
||||
// Return true to indicate undetermined policy, require user interaction
|
||||
|
@ -35,8 +34,8 @@ class SuRequestHandler(
|
|||
return false
|
||||
|
||||
// Never allow com.topjohnwu.magisk (could be malware)
|
||||
if (policy.packageName == BuildConfig.APPLICATION_ID) {
|
||||
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
|
||||
if (pkgInfo.packageName == BuildConfig.APPLICATION_ID) {
|
||||
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID} >/dev/null 2>&1)&").exec()
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -54,52 +53,55 @@ class SuRequestHandler(
|
|||
return true
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
if (::output.isInitialized)
|
||||
output.close()
|
||||
}
|
||||
|
||||
private class SuRequestError : IOException()
|
||||
|
||||
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val name = intent.getStringExtra("fifo") ?: throw SuRequestError()
|
||||
val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
|
||||
output = DataOutputStream(FileOutputStream(name).buffered())
|
||||
policy = uid.toPolicy(pm)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is IOException, is PackageManager.NameNotFoundException -> {
|
||||
Timber.e(e)
|
||||
runCatching { close() }
|
||||
false
|
||||
}
|
||||
else -> throw e // Unexpected error
|
||||
}
|
||||
private suspend fun init(intent: Intent): Boolean {
|
||||
val uid = intent.getIntExtra("uid", -1)
|
||||
val pid = intent.getIntExtra("pid", -1)
|
||||
val fifo = intent.getStringExtra("fifo")
|
||||
if (uid <= 0 || pid <= 0 || fifo == null) {
|
||||
Timber.e("Unexpected extras: uid=[${uid}], pid=[${pid}], fifo=[${fifo}]")
|
||||
return false
|
||||
}
|
||||
output = File(fifo)
|
||||
policy = SuPolicy(uid)
|
||||
try {
|
||||
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
||||
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||
// We only fill in sharedUserId and leave other fields uninitialized
|
||||
sharedUserId = name.split(":")[0]
|
||||
}
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Timber.e(e)
|
||||
respond(SuPolicy.DENY, -1)
|
||||
return false
|
||||
}
|
||||
if (!output.canWrite()) {
|
||||
Timber.e("Cannot write to $output")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun respond(action: Int, time: Int) {
|
||||
suspend fun respond(action: Int, time: Int) {
|
||||
val until = if (time > 0)
|
||||
TimeUnit.MILLISECONDS.toSeconds(now) + TimeUnit.MINUTES.toSeconds(time.toLong())
|
||||
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) +
|
||||
TimeUnit.MINUTES.toSeconds(time.toLong())
|
||||
else
|
||||
time.toLong()
|
||||
|
||||
policy.policy = action
|
||||
policy.until = until
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
output.writeInt(policy.policy)
|
||||
output.flush()
|
||||
DataOutputStream(FileOutputStream(output)).use {
|
||||
it.writeInt(policy.policy)
|
||||
it.flush()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
runCatching { close() }
|
||||
if (until >= 0)
|
||||
policyDB.update(policy)
|
||||
}
|
||||
if (until >= 0) {
|
||||
policyDB.update(policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.os.Bundle
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.NOPList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object TestHandler {
|
||||
|
||||
fun run(method: String): Bundle {
|
||||
val r = Bundle()
|
||||
|
||||
fun setup(): Boolean {
|
||||
val nop = NOPList.getInstance()
|
||||
return runBlocking {
|
||||
MagiskInstaller.Emulator(nop, nop).exec()
|
||||
}
|
||||
}
|
||||
|
||||
fun test(): Boolean {
|
||||
// Make sure Zygisk works correctly
|
||||
if (!Info.isZygiskEnabled) {
|
||||
r.putString("reason", "zygisk not enabled")
|
||||
return false
|
||||
}
|
||||
|
||||
// Make sure the Magisk app can get root
|
||||
val shell = Shell.getShell()
|
||||
if (!shell.isRoot) {
|
||||
r.putString("reason", "shell not root")
|
||||
return false
|
||||
}
|
||||
|
||||
// Make sure the root service is running
|
||||
RootUtils.Connection.await()
|
||||
|
||||
// Clear existing grant for ADB shell
|
||||
runBlocking {
|
||||
ServiceLocator.policyDB.delete(2000)
|
||||
Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW
|
||||
Config.prefs.edit().commit()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val b = runCatching {
|
||||
when (method) {
|
||||
"setup" -> setup()
|
||||
"test" -> test()
|
||||
else -> {
|
||||
r.putString("reason", "unknown method")
|
||||
false
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
r.putString("reason", it.stackTraceToString())
|
||||
false
|
||||
}
|
||||
|
||||
r.putBoolean("result", b)
|
||||
return r
|
||||
}
|
||||
}
|
|
@ -3,11 +3,11 @@ package com.topjohnwu.magisk.core.tasks
|
|||
import android.net.Uri
|
||||
import androidx.core.net.toFile
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.unzip
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -26,7 +26,7 @@ open class FlashZip(
|
|||
private lateinit var zipFile: File
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun flash(): Boolean {
|
||||
private suspend fun flash(): Boolean {
|
||||
installDir.deleteRecursively()
|
||||
installDir.mkdirs()
|
||||
|
||||
|
@ -47,13 +47,13 @@ open class FlashZip(
|
|||
}
|
||||
}
|
||||
|
||||
val isValid = runCatching {
|
||||
val isValid = try {
|
||||
zipFile.unzip(installDir, "META-INF/com/google/android", true)
|
||||
val script = File(installDir, "updater-script")
|
||||
script.readText().contains("#MAGISK")
|
||||
}.getOrElse {
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unzip error")
|
||||
throw it
|
||||
throw e
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
|
|
|
@ -4,22 +4,22 @@ 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.ktx.await
|
||||
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.utils.AXML
|
||||
import com.topjohnwu.magisk.core.utils.Keygen
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.signing.JarMap
|
||||
import com.topjohnwu.magisk.signing.SignApk
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Runnable
|
||||
|
@ -30,6 +30,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 +40,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,24 +65,91 @@ 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) }
|
||||
val keys = Keygen(context)
|
||||
val keys = Keygen()
|
||||
SignApk.sign(keys.cert, keys.key, jar, out)
|
||||
return true
|
||||
}
|
||||
|
@ -94,11 +161,9 @@ 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.APK_URI(self), flag)
|
||||
activity.grantUriPermission(pkg, Provider.PREFS_URI(self), flag)
|
||||
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
||||
intent.putExtra(Const.Key.PREV_PKG, self)
|
||||
activity.startActivity(intent)
|
||||
activity.finish()
|
||||
|
@ -107,13 +172,10 @@ object HideAPK {
|
|||
private suspend 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,11 +191,12 @@ object HideAPK {
|
|||
launchApp(activity, pkg)
|
||||
}
|
||||
|
||||
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
|
||||
if (Shell.su(cmd).exec().isSuccess) return true
|
||||
Config.suManager = pkg
|
||||
val cmd = "adb_pm_install $repack $pkg"
|
||||
if (Shell.cmd(cmd).exec().isSuccess) return true
|
||||
|
||||
try {
|
||||
session.install(activity, repack)
|
||||
repack.inputStream().copyAndClose(session.openStream(activity))
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
|
@ -152,7 +215,7 @@ object HideAPK {
|
|||
}
|
||||
val onFailure = Runnable {
|
||||
dialog.dismiss()
|
||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
patchAndHide(activity, label, onFailure)
|
||||
|
@ -170,18 +233,19 @@ object HideAPK {
|
|||
}
|
||||
val onFailure = Runnable {
|
||||
dialog.dismiss()
|
||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
val apk = StubApk.current(activity)
|
||||
val session = APKInstall.startSession(activity, APPLICATION_ID, onFailure) {
|
||||
launchApp(activity, APPLICATION_ID)
|
||||
dialog.dismiss()
|
||||
}
|
||||
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
|
||||
if (Shell.su(cmd).await().isSuccess) return
|
||||
Config.suManager = ""
|
||||
val cmd = "adb_pm_install $apk $APPLICATION_ID"
|
||||
if (Shell.cmd(cmd).await().isSuccess) return
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
session.install(activity, apk)
|
||||
apk.inputStream().copyAndClose(session.openStream(activity))
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return@withContext false
|
||||
|
@ -191,4 +255,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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,38 @@
|
|||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Process
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.system.OsConstants.O_WRONLY
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.AppApkPath
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.core.ktx.reboot
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ktx.reboot
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.signing.SignBoot
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.internal.NOPList
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import com.topjohnwu.superuser.nio.ExtendedFile
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.jpountz.lz4.LZ4FrameInputStream
|
||||
|
@ -37,29 +45,34 @@ 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
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
abstract class MagiskInstallImpl protected constructor(
|
||||
protected val console: MutableList<String> = NOPList.getInstance(),
|
||||
private val logs: MutableList<String> = NOPList.getInstance()
|
||||
) {
|
||||
|
||||
protected var installDir = File("xxx")
|
||||
private lateinit var srcBoot: File
|
||||
protected lateinit var installDir: ExtendedFile
|
||||
private lateinit var srcBoot: ExtendedFile
|
||||
|
||||
private val shell = Shell.getShell()
|
||||
private val service get() = ServiceLocator.networkService
|
||||
protected val context get() = ServiceLocator.deContext
|
||||
private val useRootDir = shell.isRoot && Info.noDataExec
|
||||
|
||||
private val rootFS get() = RootUtils.fs
|
||||
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
|
||||
}
|
||||
srcBoot = SuFile(bootPath)
|
||||
srcBoot = rootFS.getFile(bootPath)
|
||||
console.add("- Target image: $bootPath")
|
||||
return true
|
||||
}
|
||||
|
@ -77,36 +90,39 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
}
|
||||
srcBoot = SuFile(bootPath)
|
||||
srcBoot = rootFS.getFile(bootPath)
|
||||
console.add("- Target image: $bootPath")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun extractFiles(): Boolean {
|
||||
private suspend fun extractFiles(): Boolean {
|
||||
console.add("- Device platform: ${Const.CPU_ABI}")
|
||||
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
|
||||
installDir = File(context.filesDir.parent, "install")
|
||||
installDir = localFS.getFile(context.filesDir.parent, "install")
|
||||
installDir.deleteRecursively()
|
||||
installDir.mkdirs()
|
||||
|
||||
try {
|
||||
// Extract binaries
|
||||
if (isRunningAsStub) {
|
||||
val zf = ZipFile(StubApk.current(context))
|
||||
ZipFile(StubApk.current(context)).use { zf ->
|
||||
zf.entries().asSequence().filter {
|
||||
!it.isDirectory && it.name.startsWith("/lib/${Const.CPU_ABI}/")
|
||||
}.forEach {
|
||||
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
|
||||
val name = n.substring(3, n.length - 3)
|
||||
val dest = File(installDir, name)
|
||||
zf.getInputStream(it).writeTo(dest)
|
||||
dest.setExecutable(true)
|
||||
}
|
||||
|
||||
// Also extract magisk32 on non 64-bit only 64-bit devices
|
||||
val is32lib = Const.CPU_ABI_32?.let {
|
||||
{ entry: ZipEntry -> entry.name == "lib/$it/libmagisk32.so" }
|
||||
} ?: { false }
|
||||
|
||||
zf.entries().asSequence().filter {
|
||||
!it.isDirectory && (it.name.startsWith("lib/${Const.CPU_ABI}/") || is32lib(it))
|
||||
}.forEach {
|
||||
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
|
||||
val name = n.substring(3, n.length - 3)
|
||||
val dest = File(installDir, name)
|
||||
zf.getInputStream(it).writeTo(dest)
|
||||
val abi32 = Const.CPU_ABI_32
|
||||
if (Process.is64Bit() && abi32 != null) {
|
||||
val magisk32 = File(installDir, "magisk32")
|
||||
zf.getInputStream(ZipEntry("lib/$abi32/libmagisk.so")).writeTo(magisk32)
|
||||
magisk32.setExecutable(true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val info = context.applicationInfo
|
||||
|
@ -114,20 +130,21 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
name.startsWith("lib") && name.endsWith(".so")
|
||||
} ?: emptyArray()
|
||||
|
||||
// Also symlink magisk32 on non 64-bit only 64-bit devices
|
||||
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir").get(info) as String?
|
||||
if (lib32 != null) {
|
||||
libs += File(lib32, "libmagisk32.so")
|
||||
}
|
||||
|
||||
for (lib in libs) {
|
||||
val name = lib.name.substring(3, lib.name.length - 3)
|
||||
Os.symlink(lib.path, "$installDir/$name")
|
||||
}
|
||||
|
||||
// Also symlink magisk32 on 64-bit devices that supports 32-bit
|
||||
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir")
|
||||
.get(info) as String?
|
||||
if (lib32 != null) {
|
||||
Os.symlink("$lib32/libmagisk.so", "$installDir/magisk32");
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
@ -146,7 +163,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
|
||||
if (useRootDir) {
|
||||
// Move everything to tmpfs to workaround Samsung bullshit
|
||||
SuFile(Const.TMPDIR).also {
|
||||
rootFS.getFile(Const.TMPDIR).also {
|
||||
arrayOf(
|
||||
"rm -rf $it",
|
||||
"mkdir -p $it",
|
||||
|
@ -160,109 +177,221 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
return true
|
||||
}
|
||||
|
||||
// Optimization for SuFile I/O streams to skip an internal trial and error
|
||||
private fun installDirFile(name: String): File {
|
||||
return if (useRootDir)
|
||||
SuFile(installDir, name)
|
||||
else
|
||||
File(installDir, name)
|
||||
}
|
||||
|
||||
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
|
||||
src.copyTo(out)
|
||||
}
|
||||
private suspend fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyAll(it) }
|
||||
|
||||
private fun newTarEntry(name: String, size: Long): TarEntry {
|
||||
console.add("-- Writing: $name")
|
||||
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun processTar(input: InputStream, output: OutputStream): OutputStream {
|
||||
console.add("- Processing tar file")
|
||||
val tarOut = TarOutputStream(output)
|
||||
TarInputStream(input).use { tarIn ->
|
||||
lateinit var entry: TarEntry
|
||||
|
||||
fun decompressedStream(): InputStream {
|
||||
val src = if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
|
||||
return object : FilterInputStream(src) {
|
||||
override fun available() = 0 /* Workaround bug in LZ4FrameInputStream */
|
||||
override fun close() { /* Never close src stream */ }
|
||||
}
|
||||
}
|
||||
|
||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||
if (entry.name.startsWith("boot.img") ||
|
||||
(Config.recovery && entry.name.contains("recovery.img"))) {
|
||||
val name = entry.name.replace(".lz4", "")
|
||||
console.add("-- Extracting: $name")
|
||||
|
||||
val extract = installDirFile(name)
|
||||
decompressedStream().cleanPump(SuFileOutputStream.open(extract))
|
||||
} else if (entry.name.contains("vbmeta.img")) {
|
||||
val rawData = decompressedStream().readBytes()
|
||||
// Valid vbmeta.img should be at least 256 bytes
|
||||
if (rawData.size < 256)
|
||||
continue
|
||||
|
||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
||||
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||
console.add("-- Patching: vbmeta.img")
|
||||
ByteBuffer.wrap(rawData).putInt(120, 3)
|
||||
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
|
||||
tarOut.write(rawData)
|
||||
} else {
|
||||
console.add("-- Copying: ${entry.name}")
|
||||
tarOut.putNextEntry(entry)
|
||||
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
||||
}
|
||||
}
|
||||
}
|
||||
val boot = installDirFile("boot.img")
|
||||
val recovery = installDirFile("recovery.img")
|
||||
if (Config.recovery && recovery.exists() && boot.exists()) {
|
||||
// Install to recovery
|
||||
srcBoot = recovery
|
||||
// Repack boot image to prevent auto restore
|
||||
arrayOf(
|
||||
"cd $installDir",
|
||||
"chmod -R 755 .",
|
||||
"./magiskboot unpack boot.img",
|
||||
"./magiskboot repack boot.img",
|
||||
"cat new-boot.img > boot.img",
|
||||
"./magiskboot cleanup",
|
||||
"rm -f new-boot.img",
|
||||
"cd /").sh()
|
||||
SuFileInputStream.open(boot).use {
|
||||
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
|
||||
it.copyTo(tarOut)
|
||||
}
|
||||
boot.delete()
|
||||
} else {
|
||||
if (!boot.exists()) {
|
||||
console.add("! No boot image found")
|
||||
throw IOException()
|
||||
}
|
||||
srcBoot = boot
|
||||
}
|
||||
return tarOut
|
||||
private class NoAvailableStream(s: InputStream) : FilterInputStream(s) {
|
||||
// Make sure available is never called on the actual stream and always return 0
|
||||
// 1. Workaround bug in LZ4FrameInputStream
|
||||
// 2. Reduce max buffer size to prevent OOM
|
||||
override fun available() = 0
|
||||
}
|
||||
|
||||
private fun handleFile(uri: Uri): Boolean {
|
||||
private class NoBootException : IOException()
|
||||
|
||||
@Throws(IOException::class)
|
||||
private suspend fun processTar(tarIn: TarInputStream, tarOut: TarOutputStream): ExtendedFile {
|
||||
console.add("- Processing tar file")
|
||||
lateinit var entry: TarEntry
|
||||
|
||||
fun decompressedStream(): InputStream {
|
||||
val stream = if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
|
||||
return NoAvailableStream(stream)
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
val extract = installDir.getChildFile(name)
|
||||
decompressedStream().copyAndCloseOut(extract.newOutputStream())
|
||||
} else if (entry.name.contains("vbmeta.img")) {
|
||||
val rawData = decompressedStream().readBytes()
|
||||
// Valid vbmeta.img should be at least 256 bytes
|
||||
if (rawData.size < 256)
|
||||
continue
|
||||
|
||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
||||
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||
console.add("-- Patching: vbmeta.img")
|
||||
ByteBuffer.wrap(rawData).putInt(120, 3)
|
||||
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
|
||||
tarOut.write(rawData)
|
||||
// vbmeta partition exist, disable boot vbmeta patch
|
||||
Info.patchBootVbmeta = false
|
||||
} else if (entry.name.contains("userdata.img")) {
|
||||
continue
|
||||
} else {
|
||||
console.add("-- Copying: ${entry.name}")
|
||||
tarOut.putNextEntry(entry)
|
||||
tarIn.copyAll(tarOut, bufferSize = 1024 * 1024)
|
||||
}
|
||||
}
|
||||
|
||||
val boot = installDir.getChildFile("boot.img")
|
||||
val initBoot = installDir.getChildFile("init_boot.img")
|
||||
val recovery = installDir.getChildFile("recovery.img")
|
||||
|
||||
suspend fun ExtendedFile.copyToTar() {
|
||||
newInputStream().use {
|
||||
tarOut.putNextEntry(newTarEntry(name, length()))
|
||||
it.copyAll(tarOut)
|
||||
}
|
||||
delete()
|
||||
}
|
||||
|
||||
// Patch priority: recovery > init_boot > boot
|
||||
return when {
|
||||
recovery.exists() -> {
|
||||
if (boot.exists()) {
|
||||
// Repack boot image to prevent auto restore
|
||||
arrayOf(
|
||||
"cd $installDir",
|
||||
"chmod -R 755 .",
|
||||
"./magiskboot unpack boot.img",
|
||||
"./magiskboot repack boot.img",
|
||||
"cat new-boot.img > boot.img",
|
||||
"./magiskboot cleanup",
|
||||
"rm -f new-boot.img",
|
||||
"cd /").sh()
|
||||
boot.copyToTar()
|
||||
}
|
||||
recovery
|
||||
}
|
||||
initBoot.exists() -> {
|
||||
if (boot.exists())
|
||||
boot.copyToTar()
|
||||
initBoot
|
||||
}
|
||||
boot.exists() -> boot
|
||||
else -> throw NoBootException()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private suspend fun processZip(zipIn: ZipInputStream): ExtendedFile {
|
||||
console.add("- Processing zip file")
|
||||
val boot = installDir.getChildFile("boot.img")
|
||||
val initBoot = installDir.getChildFile("init_boot.img")
|
||||
lateinit var entry: ZipEntry
|
||||
while (zipIn.nextEntry?.also { entry = it } != null) {
|
||||
if (entry.isDirectory) continue
|
||||
when (entry.name.substringAfterLast('/')) {
|
||||
"payload.bin" -> {
|
||||
try {
|
||||
return processPayload(zipIn)
|
||||
} catch (e: IOException) {
|
||||
// No boot image in payload.bin, continue to find boot images
|
||||
}
|
||||
}
|
||||
"init_boot.img" -> {
|
||||
console.add("- Extracting init_boot.img")
|
||||
zipIn.copyAndCloseOut(initBoot.newOutputStream())
|
||||
return initBoot
|
||||
}
|
||||
"boot.img" -> {
|
||||
console.add("- Extracting boot.img")
|
||||
zipIn.copyAndCloseOut(boot.newOutputStream())
|
||||
// Don't return here since there might be an init_boot.img
|
||||
}
|
||||
}
|
||||
}
|
||||
if (boot.exists()) {
|
||||
return boot
|
||||
} else {
|
||||
throw NoBootException()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun processPayload(input: InputStream): ExtendedFile {
|
||||
var fifo: File? = null
|
||||
try {
|
||||
console.add("- Processing payload.bin")
|
||||
fifo = File.createTempFile("payload-fifo-", null, installDir)
|
||||
fifo.delete()
|
||||
Os.mkfifo(fifo.path, 420 /* 0644 */)
|
||||
|
||||
// Enqueue the shell command first, or the subsequent FIFO open will block
|
||||
val future = arrayOf(
|
||||
"cd $installDir",
|
||||
"./magiskboot extract $fifo",
|
||||
"cd /"
|
||||
).eq()
|
||||
|
||||
val fd = Os.open(fifo.path, O_WRONLY, 0)
|
||||
try {
|
||||
val bufSize = 1024 * 1024
|
||||
val buf = ByteBuffer.allocate(bufSize)
|
||||
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
|
||||
while (buf.hasRemaining()) {
|
||||
try {
|
||||
Os.write(fd, buf)
|
||||
} catch (e: ErrnoException) {
|
||||
if (e.errno != OsConstants.EPIPE)
|
||||
throw e
|
||||
// If SIGPIPE, then the other side is closed, we're done
|
||||
break
|
||||
}
|
||||
if (!buf.hasRemaining()) {
|
||||
buf.limit(bufSize)
|
||||
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Os.close(fd)
|
||||
}
|
||||
|
||||
val success = try { future.get().isSuccess } catch (e: Exception) { false }
|
||||
if (!success) {
|
||||
console.add("! Error while extracting payload.bin")
|
||||
throw IOException()
|
||||
}
|
||||
val boot = installDir.getChildFile("boot.img")
|
||||
val initBoot = installDir.getChildFile("init_boot.img")
|
||||
return when {
|
||||
initBoot.exists() -> {
|
||||
console.add("-- Extract init_boot.img")
|
||||
initBoot
|
||||
}
|
||||
boot.exists() -> {
|
||||
console.add("-- Extract boot.img")
|
||||
boot
|
||||
}
|
||||
else -> {
|
||||
throw NoBootException()
|
||||
}
|
||||
}
|
||||
} catch (e: ErrnoException) {
|
||||
throw IOException(e)
|
||||
} finally {
|
||||
fifo?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleFile(uri: Uri): Boolean {
|
||||
val outStream: OutputStream
|
||||
var outFile: MediaStoreUtils.UriFile? = null
|
||||
val outFile: MediaStoreUtils.UriFile
|
||||
|
||||
// Process input file
|
||||
try {
|
||||
uri.inputStream().buffered().use { src ->
|
||||
src.mark(500)
|
||||
val magic = ByteArray(5)
|
||||
if (src.skip(257) != 257L || src.read(magic) != magic.size) {
|
||||
PushbackInputStream(uri.inputStream(), 512).use { src ->
|
||||
val head = ByteArray(512)
|
||||
if (src.read(head) != head.size) {
|
||||
console.add("! Invalid input file")
|
||||
return false
|
||||
}
|
||||
src.reset()
|
||||
src.unread(head)
|
||||
|
||||
val magic = head.copyOf(4)
|
||||
val tarMagic = Arrays.copyOfRange(head, 257, 262)
|
||||
|
||||
val alpha = "abcdefghijklmnopqrstuvwxyz"
|
||||
val alphaNum = "$alpha${alpha.uppercase(Locale.ROOT)}0123456789"
|
||||
|
@ -274,40 +403,69 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
toString()
|
||||
}
|
||||
|
||||
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
||||
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
|
||||
// tar file
|
||||
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
||||
processTar(src, outFile!!.uri.outputStream())
|
||||
outStream = TarOutputStream(outFile.uri.outputStream())
|
||||
|
||||
try {
|
||||
processTar(TarInputStream(src), outStream)
|
||||
} catch (e: IOException) {
|
||||
outStream.close()
|
||||
outFile.delete()
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
// raw image
|
||||
srcBoot = installDirFile("boot.img")
|
||||
console.add("- Copying image to cache")
|
||||
src.cleanPump(SuFileOutputStream.open(srcBoot))
|
||||
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
||||
outFile!!.uri.outputStream()
|
||||
outStream = outFile.uri.outputStream()
|
||||
|
||||
try {
|
||||
if (magic.contentEquals("CrAU".toByteArray())) {
|
||||
processPayload(src)
|
||||
} else if (magic.contentEquals("PK\u0003\u0004".toByteArray())) {
|
||||
processZip(ZipInputStream(src))
|
||||
} else {
|
||||
console.add("- Copying image to cache")
|
||||
installDir.getChildFile("boot.img").also {
|
||||
src.copyAndCloseOut(it.newOutputStream())
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
outStream.close()
|
||||
outFile.delete()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
if (e is NoBootException)
|
||||
console.add("! No boot image found")
|
||||
console.add("! Process error")
|
||||
outFile?.delete()
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// Patch file
|
||||
if (!patchBoot()) {
|
||||
outFile!!.delete()
|
||||
outFile.delete()
|
||||
return false
|
||||
}
|
||||
|
||||
// Output file
|
||||
try {
|
||||
val newBoot = installDirFile("new-boot.img")
|
||||
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()))
|
||||
}
|
||||
SuFileInputStream.open(newBoot).cleanPump(outStream)
|
||||
newBoot.newInputStream().copyAndClose(outStream)
|
||||
newBoot.delete()
|
||||
|
||||
console.add("")
|
||||
|
@ -317,7 +475,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
console.add("****************************")
|
||||
} catch (e: IOException) {
|
||||
console.add("! Failed to output to $outFile")
|
||||
outFile!!.delete()
|
||||
outFile.delete()
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
@ -330,23 +488,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
}
|
||||
|
||||
private fun patchBoot(): Boolean {
|
||||
var isSigned = false
|
||||
if (srcBoot.let { it !is SuFile || !it.isCharacter }) {
|
||||
try {
|
||||
SuFileInputStream.open(srcBoot).use {
|
||||
if (SignBoot.verifySignature(it, null)) {
|
||||
isSigned = true
|
||||
console.add("- Boot image is signed with AVB 1.0")
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to check signature")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val newBoot = installDirFile("new-boot.img")
|
||||
val newBoot = installDir.getChildFile("new-boot.img")
|
||||
if (!useRootDir) {
|
||||
// Create output files before hand
|
||||
newBoot.createNewFile()
|
||||
|
@ -357,38 +499,20 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
"cd $installDir",
|
||||
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
|
||||
"KEEPVERITY=${Config.keepVerity} " +
|
||||
"PATCHVBMETAFLAG=${Config.patchVbmeta} " +
|
||||
"PATCHVBMETAFLAG=${Info.patchBootVbmeta} " +
|
||||
"RECOVERYMODE=${Config.recovery} " +
|
||||
"LEGACYSAR=${Info.legacySAR} " +
|
||||
"sh boot_patch.sh $srcBoot")
|
||||
val isSuccess = cmds.sh().isSuccess
|
||||
|
||||
if (!cmds.sh().isSuccess)
|
||||
return false
|
||||
shell.newJob().add("./magiskboot cleanup", "cd /").exec()
|
||||
|
||||
val job = shell.newJob().add("./magiskboot cleanup", "cd /")
|
||||
|
||||
if (isSigned) {
|
||||
console.add("- Signing boot image with verity keys")
|
||||
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
||||
try {
|
||||
val src = SuFileInputStream.open(newBoot).buffered()
|
||||
val out = signed.outputStream().buffered()
|
||||
withStreams(src, out) { _, _ ->
|
||||
SignBoot.doSignature(null, null, src, out, "/boot")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to sign image")
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
job.add("cat $signed > $newBoot", "rm -f $signed")
|
||||
}
|
||||
job.exec()
|
||||
return true
|
||||
return isSuccess
|
||||
}
|
||||
|
||||
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
||||
|
||||
private fun postOTA(): Boolean {
|
||||
private suspend fun postOTA(): Boolean {
|
||||
try {
|
||||
val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
|
||||
context.assets.open("bootctl").writeTo(bootctl)
|
||||
|
@ -399,25 +523,27 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
return false
|
||||
}
|
||||
|
||||
console.add("***************************************")
|
||||
console.add("*************************************************************")
|
||||
console.add(" Next reboot will boot to second slot!")
|
||||
console.add("***************************************")
|
||||
console.add(" Go back to System Updates and press Restart to complete OTA")
|
||||
console.add("*************************************************************")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun Array<String>.eq() = shell.newJob().add(*this).to(console, logs).enqueue()
|
||||
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
|
||||
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
|
||||
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
|
||||
|
||||
protected fun doPatchFile(patchFile: Uri) = extractFiles() && handleFile(patchFile)
|
||||
protected suspend fun patchFile(file: Uri) = extractFiles() && handleFile(file)
|
||||
|
||||
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
|
||||
protected suspend fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
|
||||
|
||||
protected suspend fun secondSlot() =
|
||||
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
||||
|
||||
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
||||
protected suspend fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
||||
|
||||
protected fun uninstall() = "run_uninstaller $AppApkPath".sh().isSuccess
|
||||
|
||||
|
@ -425,20 +551,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -463,7 +584,7 @@ abstract class MagiskInstaller(
|
|||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(console, logs) {
|
||||
override suspend fun operations() = doPatchFile(uri)
|
||||
override suspend fun operations() = patchFile(uri)
|
||||
}
|
||||
|
||||
class SecondSlot(
|
||||
|
@ -510,7 +631,7 @@ abstract class MagiskInstaller(
|
|||
override suspend fun exec(): Boolean {
|
||||
val success = super.exec()
|
||||
callback()
|
||||
Utils.toast(
|
||||
context.toast(
|
||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
|
|
|
@ -4,7 +4,6 @@ import java.io.ByteArrayOutputStream
|
|||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder.LITTLE_ENDIAN
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
class AXML(b: ByteArray) {
|
||||
|
||||
|
@ -30,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 {
|
||||
|
@ -43,7 +42,6 @@ class AXML(b: ByteArray) {
|
|||
return -1
|
||||
}
|
||||
|
||||
var patch = false
|
||||
val start = findStringPool()
|
||||
if (start < 0)
|
||||
return false
|
||||
|
@ -58,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
|
||||
|
@ -104,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
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
|
||||
object BiometricHelper {
|
||||
|
||||
private val mgr by lazy { BiometricManager.from(AppContext) }
|
||||
|
||||
val isSupported get() = when (mgr.canAuthenticate()) {
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val isEnabled: Boolean get() {
|
||||
val enabled = Config.suBiometric
|
||||
if (enabled && !isSupported) {
|
||||
Config.suBiometric = false
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
fun authenticate(
|
||||
activity: FragmentActivity,
|
||||
onError: () -> Unit = {},
|
||||
onSuccess: () -> Unit): BiometricPrompt {
|
||||
val prompt = BiometricPrompt(activity,
|
||||
ContextCompat.getMainExecutor(activity),
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
onError()
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
onError()
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
)
|
||||
val info = BiometricPrompt.PromptInfo.Builder()
|
||||
.setConfirmationRequired(true)
|
||||
.setDeviceCredentialAllowed(false)
|
||||
.setTitle(activity.getString(R.string.authenticate))
|
||||
.setNegativeButtonText(activity.getString(android.R.string.cancel))
|
||||
.build()
|
||||
prompt.authenticate(info)
|
||||
return prompt
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Runnable
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.AbstractExecutorService
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Base64
|
||||
import android.util.Base64OutputStream
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.signing.CryptoUtils.readCertificate
|
||||
import com.topjohnwu.magisk.signing.CryptoUtils.readPrivateKey
|
||||
import com.topjohnwu.magisk.signing.KeyData
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.Random
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
|
@ -29,13 +25,11 @@ private interface CertKeyProvider {
|
|||
val key: PrivateKey
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class Keygen(context: Context) : CertKeyProvider {
|
||||
class Keygen : CertKeyProvider {
|
||||
|
||||
companion object {
|
||||
private const val ALIAS = "magisk"
|
||||
private val PASSWORD get() = "magisk".toCharArray()
|
||||
private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81"
|
||||
private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android"
|
||||
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||
}
|
||||
|
@ -43,49 +37,9 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
||||
private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
|
||||
|
||||
override val cert get() = provider.cert
|
||||
override val key get() = provider.key
|
||||
|
||||
private val provider: CertKeyProvider
|
||||
|
||||
inner class KeyStoreProvider :
|
||||
CertKeyProvider {
|
||||
private val ks by lazy { init() }
|
||||
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate }
|
||||
override val key by lazy { ks.getKey(
|
||||
ALIAS,
|
||||
PASSWORD
|
||||
) as PrivateKey }
|
||||
}
|
||||
|
||||
class TestProvider : CertKeyProvider {
|
||||
override val cert by lazy {
|
||||
readCertificate(ByteArrayInputStream(KeyData.testCert()))
|
||||
}
|
||||
override val key by lazy {
|
||||
readPrivateKey(ByteArrayInputStream(KeyData.testKey()))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val pm = context.packageManager
|
||||
val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
||||
val sig = info.signatures[0]
|
||||
val digest = MessageDigest.getInstance("SHA1")
|
||||
val chksum = digest.digest(sig.toByteArray())
|
||||
|
||||
val sb = StringBuilder()
|
||||
for (b in chksum) {
|
||||
sb.append("%02x".format(0xFF and b.toInt()))
|
||||
}
|
||||
|
||||
provider = if (sb.toString() == TESTKEY_CERT) {
|
||||
// The app was signed by the test key, continue to use it (legacy mode)
|
||||
TestProvider()
|
||||
} else {
|
||||
KeyStoreProvider()
|
||||
}
|
||||
}
|
||||
private val ks = init()
|
||||
override val cert = ks.getCertificate(ALIAS) as X509Certificate
|
||||
override val key = ks.getKey(ALIAS, PASSWORD) as PrivateKey
|
||||
|
||||
private fun init(): KeyStore {
|
||||
val raw = Config.keyStoreRaw
|
||||
|
@ -93,12 +47,8 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||
if (raw.isEmpty()) {
|
||||
ks.load(null)
|
||||
} else {
|
||||
GZIPInputStream(Base64.decode(raw,
|
||||
BASE64_FLAG
|
||||
).inputStream()).use {
|
||||
ks.load(it,
|
||||
PASSWORD
|
||||
)
|
||||
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use {
|
||||
ks.load(it, PASSWORD)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,22 +59,19 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||
// Generate new private key and certificate
|
||||
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||
val dname = X500Name(DNAME)
|
||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
||||
start.time, end.time, dname, kp.public)
|
||||
val builder = X509v3CertificateBuilder(
|
||||
dname, BigInteger(160, Random()),
|
||||
start.time, end.time, Locale.ROOT, dname,
|
||||
SubjectPublicKeyInfo.getInstance(kp.public.encoded)
|
||||
)
|
||||
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
||||
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||
|
||||
// Store them into keystore
|
||||
ks.setKeyEntry(
|
||||
ALIAS, kp.private,
|
||||
PASSWORD, arrayOf(cert))
|
||||
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
|
||||
val bytes = ByteArrayOutputStream()
|
||||
GZIPOutputStream(Base64OutputStream(bytes,
|
||||
BASE64_FLAG
|
||||
)).use {
|
||||
ks.store(it,
|
||||
PASSWORD
|
||||
)
|
||||
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
|
||||
ks.store(it, PASSWORD)
|
||||
}
|
||||
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||
|
||||
|
|
|
@ -6,13 +6,13 @@ import android.annotation.SuppressLint
|
|||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.ActivityTracker
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.createNewResources
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import java.util.Locale
|
||||
|
||||
var currentLocale: Locale = Locale.getDefault()
|
||||
|
||||
|
@ -28,6 +28,11 @@ withContext(Dispatchers.Default) {
|
|||
// Create a completely new resource to prevent cross talk over active configs
|
||||
val res = createNewResources()
|
||||
|
||||
fun changeLocale(locale: Locale) {
|
||||
res.configuration.setLocale(locale)
|
||||
res.updateConfiguration(res.configuration, res.displayMetrics)
|
||||
}
|
||||
|
||||
val locales = ArrayList<String>().apply {
|
||||
// Add default locale
|
||||
add("en")
|
||||
|
@ -41,13 +46,13 @@ withContext(Dispatchers.Default) {
|
|||
}.map {
|
||||
Locale.forLanguageTag(it)
|
||||
}.distinctBy {
|
||||
res.setLocale(it)
|
||||
changeLocale(it)
|
||||
res.getString(compareId)
|
||||
}.sortedWith { a, b ->
|
||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||
}
|
||||
|
||||
res.setLocale(defaultLocale)
|
||||
changeLocale(defaultLocale)
|
||||
val defName = res.getString(R.string.system_default)
|
||||
|
||||
val names = ArrayList<String>(locales.size + 1)
|
||||
|
@ -71,11 +76,6 @@ fun Resources.setConfig(config: Configuration) {
|
|||
|
||||
fun Resources.syncLocale() = setConfig(configuration)
|
||||
|
||||
fun Resources.setLocale(locale: Locale) {
|
||||
configuration.setLocale(locale)
|
||||
updateConfiguration(configuration, displayMetrics)
|
||||
}
|
||||
|
||||
fun refreshLocale() {
|
||||
val localeConfig = Config.locale
|
||||
currentLocale = when {
|
||||
|
@ -84,4 +84,5 @@ fun refreshLocale() {
|
|||
}
|
||||
Locale.setDefault(currentLocale)
|
||||
AppContext.resources.syncLocale()
|
||||
ActivityTracker.foreground?.recreate()
|
||||
}
|
||||
|
|
|
@ -11,13 +11,10 @@ import androidx.annotation.RequiresApi
|
|||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.MessageDigest
|
||||
import kotlin.experimental.and
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
object MediaStoreUtils {
|
||||
|
@ -87,7 +84,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()
|
||||
|
@ -102,6 +99,8 @@ object MediaStoreUtils {
|
|||
|
||||
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
|
||||
|
||||
fun Uri.fileDescriptor(mode: String) = cr.openFileDescriptor(this, mode) ?: throw FileNotFoundException()
|
||||
|
||||
val Uri.displayName: String get() {
|
||||
if (scheme == "file") {
|
||||
// Simple uri wrapper over file, directly get file name
|
||||
|
@ -118,24 +117,6 @@ object MediaStoreUtils {
|
|||
return this.toString()
|
||||
}
|
||||
|
||||
fun Uri.checkSum(alg: String, reference: String) = runCatching {
|
||||
this.inputStream().use {
|
||||
val digest = MessageDigest.getInstance(alg)
|
||||
it.copyTo(object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
digest.update(b.toByte())
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
digest.update(b, off, len)
|
||||
}
|
||||
})
|
||||
val sb = StringBuilder()
|
||||
digest.digest().forEach { b -> sb.append("%02x".format(b and 0xff.toByte())) }
|
||||
sb.toString() == reference
|
||||
}
|
||||
}.getOrElse { false }
|
||||
|
||||
interface UriFile {
|
||||
val uri: Uri
|
||||
fun delete(): Boolean
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
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.core.Info
|
||||
import com.topjohnwu.magisk.core.ktx.registerRuntimeReceiver
|
||||
|
||||
class NetworkObserver(context: Context): 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)
|
||||
postValue(true)
|
||||
}
|
||||
override fun onLost(network: Network) {
|
||||
activeList.remove(network)
|
||||
postValue(!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()) {
|
||||
postValue(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() {
|
||||
postValue(manager.getNetworkCapabilities(manager.activeNetwork)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) ?: false)
|
||||
}
|
||||
|
||||
private fun postValue(b: Boolean) {
|
||||
Info.remote = Info.EMPTY_REMOTE
|
||||
Info.isConnected.postValue(b)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun init(context: Context): NetworkObserver {
|
||||
return NetworkObserver(context).apply { postCurrentState() }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.topjohnwu.magisk.core.utils;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LifecycleDispatcher;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
// Use Java to bypass Kotlin internal visibility modifier
|
||||
public class ProcessLifecycle {
|
||||
public static void init(@NonNull Context context) {
|
||||
LifecycleDispatcher.init(context);
|
||||
ProcessLifecycleOwner.init$lifecycle_process_release(context);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
|
||||
class RequestAuthentication: ActivityResultContract<Unit, Boolean>() {
|
||||
|
||||
override fun createIntent(context: Context, input: Unit) =
|
||||
context.getSystemService(KeyguardManager::class.java)
|
||||
.createConfirmDeviceCredentialIntent(null, null)
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?) =
|
||||
resultCode == Activity.RESULT_OK
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class RootRegistry(stub: Any?) : RootService() {
|
||||
|
||||
constructor() : this(null) {
|
||||
// Always log full stack trace with Timber
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||
Timber.e(e)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
private val className: String? = stub?.javaClass?.name
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
// TODO: PLACEHOLDER
|
||||
return Binder()
|
||||
}
|
||||
|
||||
override fun getComponentName(): ComponentName {
|
||||
return ComponentName(packageName, className ?: javaClass.name)
|
||||
}
|
||||
|
||||
// TODO: PLACEHOLDER
|
||||
object Connection : CountDownLatch(1), ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
Timber.d("onServiceConnected")
|
||||
countDown()
|
||||
}
|
||||
override fun onNullBinding(name: ComponentName) {
|
||||
Timber.d("onServiceConnected")
|
||||
countDown()
|
||||
}
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
bind(Intent().setComponent(name), this)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var bindTask: Shell.Task? = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.system.Os
|
||||
import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.locks.AbstractQueuedSynchronizer
|
||||
|
||||
class RootUtils(stub: Any?) : RootService() {
|
||||
|
||||
private val className: String = stub?.javaClass?.name ?: javaClass.name
|
||||
private lateinit var am: ActivityManager
|
||||
|
||||
constructor() : this(null) {
|
||||
Timber.plant(object : Timber.DebugTree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
super.log(priority, "Magisk", message, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
am = getSystemService()!!
|
||||
}
|
||||
|
||||
override fun getComponentName(): ComponentName {
|
||||
return ComponentName(packageName, className)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return object : IRootUtils.Stub() {
|
||||
override fun getAppProcess(pid: Int) = safe(null) { getAppProcessImpl(pid) }
|
||||
override fun getFileSystem(): IBinder = FileSystemManager.getService()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> safe(default: T, block: () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
// The process died unexpectedly
|
||||
Timber.e(e)
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAppProcessImpl(_pid: Int): ActivityManager.RunningAppProcessInfo? {
|
||||
val procList = am.runningAppProcesses
|
||||
var pid = _pid
|
||||
while (pid > 1) {
|
||||
val proc = procList.find { it.pid == pid }
|
||||
if (proc != null)
|
||||
return proc
|
||||
|
||||
// Stop find when root process
|
||||
if (Os.stat("/proc/$pid").st_uid == 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Find PPID
|
||||
File("/proc/$pid/status").useLines {
|
||||
val line = it.find { l -> l.startsWith("PPid:") } ?: return null
|
||||
pid = line.substring(5).trim().toInt()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
object Connection : AbstractQueuedSynchronizer(), ServiceConnection {
|
||||
init {
|
||||
state = 1
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
Timber.d("onServiceConnected")
|
||||
IRootUtils.Stub.asInterface(service).let {
|
||||
obj = it
|
||||
fs = FileSystemManager.getRemote(it.fileSystem)
|
||||
}
|
||||
releaseShared(1)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
state = 1
|
||||
obj = null
|
||||
bind(Intent().setComponent(name), this)
|
||||
}
|
||||
|
||||
override fun tryAcquireShared(acquires: Int) = if (state == 0) 1 else -1
|
||||
|
||||
override fun tryReleaseShared(releases: Int): Boolean {
|
||||
// Decrement count; signal when transition to zero
|
||||
while (true) {
|
||||
val c = state
|
||||
if (c == 0)
|
||||
return false
|
||||
val n = c - 1
|
||||
if (compareAndSetState(c, n))
|
||||
return n == 0
|
||||
}
|
||||
}
|
||||
|
||||
fun await() {
|
||||
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 = FileSystemManager.getLocal()
|
||||
get() {
|
||||
Connection.await()
|
||||
return field
|
||||
}
|
||||
private set
|
||||
var obj: IRootUtils? = null
|
||||
get() {
|
||||
Connection.await()
|
||||
return field
|
||||
}
|
||||
private set
|
||||
}
|
||||
}
|
|
@ -7,20 +7,23 @@ import com.topjohnwu.magisk.core.Config
|
|||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
||||
import com.topjohnwu.magisk.ktx.rawResource
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||
import com.topjohnwu.magisk.core.ktx.rawResource
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.util.jar.JarFile
|
||||
|
||||
class ShellInit : Shell.Initializer() {
|
||||
override fun onInit(context: Context, shell: Shell): Boolean {
|
||||
if (shell.isRoot) {
|
||||
RootRegistry.bindTask?.let { shell.execTask(it) }
|
||||
RootRegistry.bindTask = null
|
||||
Info.isRooted = true
|
||||
RootUtils.bindTask?.let { shell.execTask(it) }
|
||||
RootUtils.bindTask = null
|
||||
}
|
||||
shell.newJob().apply {
|
||||
add("export ASH_STANDALONE=1")
|
||||
|
@ -33,14 +36,16 @@ class ShellInit : Shell.Initializer() {
|
|||
val bb = jar.getJarEntry("lib/${Const.CPU_ABI}/libbusybox.so")
|
||||
localBB = context.deviceProtectedContext.cachedFile("busybox")
|
||||
localBB.delete()
|
||||
jar.getInputStream(bb).writeTo(localBB)
|
||||
runBlocking {
|
||||
jar.getInputStream(bb).writeTo(localBB, dispatcher = Dispatchers.Unconfined)
|
||||
}
|
||||
localBB.setExecutable(true)
|
||||
} else {
|
||||
localBB = File(context.applicationInfo.nativeLibraryDir, "libbusybox.so")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -48,12 +53,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 {
|
||||
|
@ -72,18 +77,17 @@ 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.isSAR = getBool("SYSTEM_AS_ROOT")
|
||||
Info.ramdisk = getBool("RAMDISKEXIST")
|
||||
Info.vbmeta = getBool("VBMETAEXIST")
|
||||
Info.isAB = getBool("ISAB")
|
||||
Info.crypto = getVar("CRYPTOTYPE")
|
||||
Info.patchBootVbmeta = getBool("PATCHVBMETAFLAG")
|
||||
Info.legacySAR = getBool("LEGACYSAR")
|
||||
|
||||
// Default presets
|
||||
Config.recovery = getBool("RECOVERYMODE")
|
||||
Config.keepVerity = getBool("KEEPVERITY")
|
||||
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
||||
Config.patchVbmeta = getBool("PATCHVBMETAFLAG")
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
|
||||
class UninstallPackage : ActivityResultContract<String, Boolean>() {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val uri = Uri.Builder().scheme("package").opaquePart(input).build()
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri)
|
||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
return intent
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?) =
|
||||
resultCode == Activity.RESULT_OK
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
@ -9,14 +8,14 @@ import java.util.zip.ZipEntry
|
|||
import java.util.zip.ZipInputStream
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun File.unzip(folder: File, path: String = "", junkPath: Boolean = false) {
|
||||
suspend fun File.unzip(folder: File, path: String = "", junkPath: Boolean = false) {
|
||||
inputStream().buffered().use {
|
||||
it.unzip(folder, path, junkPath)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
||||
suspend fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
||||
try {
|
||||
val zin = ZipInputStream(this)
|
||||
var entry: ZipEntry
|
||||
|
@ -31,12 +30,12 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
|||
else
|
||||
entry.name
|
||||
|
||||
var dest = File(folder, name)
|
||||
if (!dest.parentFile!!.exists() && !dest.parentFile!!.mkdirs()) {
|
||||
dest = SuFile(folder, name)
|
||||
dest.parentFile!!.mkdirs()
|
||||
val dest = File(folder, name)
|
||||
dest.parentFile!!.let {
|
||||
if (!it.exists())
|
||||
it.mkdirs()
|
||||
}
|
||||
SuFileOutputStream.open(dest).use { out -> zin.copyTo(out) }
|
||||
dest.outputStream().use { out -> zin.copyAll(out) }
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw IOException(e)
|
||||
|
|
|
@ -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,67 +0,0 @@
|
|||
package com.topjohnwu.magisk.data.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface PreferenceModel {
|
||||
|
||||
val context: Context
|
||||
|
||||
val fileName: String
|
||||
get() = "${context.packageName}_preferences"
|
||||
|
||||
val prefs: SharedPreferences
|
||||
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||
|
||||
fun preferenceStrInt(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = false
|
||||
) = object: ReadWriteProperty<PreferenceModel, Int> {
|
||||
val base = StringProperty(name, default.toString(), commit)
|
||||
override fun getValue(thisRef: PreferenceModel, property: KProperty<*>): Int =
|
||||
base.getValue(thisRef, property).toInt()
|
||||
|
||||
override fun setValue(thisRef: PreferenceModel, property: KProperty<*>, value: Int) =
|
||||
base.setValue(thisRef, property, value.toString())
|
||||
}
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Boolean,
|
||||
commit: Boolean = false
|
||||
) = BooleanProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Float,
|
||||
commit: Boolean = false
|
||||
) = FloatProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = false
|
||||
) = IntProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Long,
|
||||
commit: Boolean = false
|
||||
) = LongProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: String,
|
||||
commit: Boolean = false
|
||||
) = StringProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Set<String>,
|
||||
commit: Boolean = false
|
||||
) = StringSetProperty(name, default, commit)
|
||||
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
|
||||
open class BindingBoundAdapter : BindingRecyclerViewAdapter<RvItem>() {
|
||||
|
||||
override fun onBindBinding(binding: ViewDataBinding, variableId: Int, layoutRes: Int, position: Int, item: RvItem) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
|
||||
item.onBindingBound(binding)
|
||||
}
|
||||
}
|
|
@ -8,9 +8,11 @@ import android.text.Spanned
|
|||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
|
@ -23,13 +25,18 @@ import androidx.databinding.BindingAdapter
|
|||
import androidx.databinding.InverseBindingAdapter
|
||||
import androidx.databinding.InverseBindingListener
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.recyclerview.widget.*
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.widget.IndeterminateCheckBox
|
||||
import kotlin.math.roundToInt
|
||||
|
@ -289,3 +296,13 @@ fun TextView.setTextColorAttr(attr: Int) {
|
|||
context.theme.resolveAttribute(attr, tv, true)
|
||||
setTextColor(tv.data)
|
||||
}
|
||||
|
||||
@BindingAdapter("android:text")
|
||||
fun TextView.setText(text: TextHolder) {
|
||||
this.text = text.getText(context.resources)
|
||||
}
|
||||
|
||||
@BindingAdapter("items", "layout")
|
||||
fun Spinner.setAdapter(items: Array<Any>, layoutRes: Int) {
|
||||
adapter = ArrayAdapter(context, layoutRes, items)
|
||||
}
|
||||
|
|
|
@ -1,38 +1,53 @@
|
|||
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 kotlin.collections.ArrayList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
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> {
|
||||
// Only expose the immutable List types
|
||||
interface DiffList<T : DiffItem<*>> : List<T> {
|
||||
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult
|
||||
|
||||
protected var list: MutableList<T> = ArrayList()
|
||||
@MainThread
|
||||
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult)
|
||||
|
||||
@WorkerThread
|
||||
suspend fun update(newItems: List<T>)
|
||||
}
|
||||
|
||||
interface FilterList<T : DiffItem<*>> : List<T> {
|
||||
fun filter(filter: (T) -> Boolean)
|
||||
|
||||
@MainThread
|
||||
fun set(newItems: List<T>)
|
||||
}
|
||||
|
||||
fun <T : DiffItem<*>> diffList(): DiffList<T> = DiffObservableList()
|
||||
|
||||
fun <T : DiffItem<*>> filterList(scope: CoroutineScope): FilterList<T> =
|
||||
FilterableDiffObservableList(scope)
|
||||
|
||||
private open class DiffObservableList<T : DiffItem<*>>
|
||||
: AbstractList<T>(), ObservableList<T>, DiffList<T>, ListUpdateCallback {
|
||||
|
||||
protected var list: List<T> = emptyList()
|
||||
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.
|
||||
*/
|
||||
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||
val frozenList = ArrayList(list)
|
||||
return doCalculateDiff(frozenList, newItems)
|
||||
override fun get(index: Int) = list[index]
|
||||
|
||||
override fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||
return doCalculateDiff(list, newItems)
|
||||
}
|
||||
|
||||
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
|
||||
|
@ -41,47 +56,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)
|
||||
override fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
||||
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
|
||||
override 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>>) {
|
||||
|
@ -92,113 +94,63 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
private class FilterableDiffObservableList<T : DiffItem<*>>(
|
||||
private val scope: CoroutineScope
|
||||
) : DiffObservableList<T>(), FilterList<T> {
|
||||
|
||||
private var sublist: List<T> = emptyList()
|
||||
private var job: Job? = null
|
||||
private var lastFilter: ((T) -> Boolean)? = null
|
||||
|
||||
// ---
|
||||
|
||||
override fun filter(filter: (T) -> Boolean) {
|
||||
lastFilter = filter
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun get(index: Int): T {
|
||||
return sublist[index]
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = sublist.size
|
||||
|
||||
@MainThread
|
||||
override fun set(newItems: List<T>) {
|
||||
onRemoved(0, sublist.size)
|
||||
list = newItems
|
||||
sublist = emptyList()
|
||||
lastFilter?.let { filter(it) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
import java.util.*
|
||||
|
||||
class FilterableDiffObservableList<T>(
|
||||
callback: Callback<T>
|
||||
) : DiffObservableList<T>(callback) {
|
||||
|
||||
var filter: ((T) -> Boolean)? = null
|
||||
set(value) {
|
||||
field = value
|
||||
queueUpdate()
|
||||
}
|
||||
@Volatile
|
||||
private var sublist: MutableList<T> = super.list
|
||||
|
||||
// ---
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = sublist.size
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||
|
||||
fun <T : AnyDiffRvItem> diffListOf() =
|
||||
DiffObservableList(DiffRvItem.callback<T>())
|
||||
|
||||
fun <T : AnyDiffRvItem> diffListOf(newItems: List<T>) =
|
||||
DiffObservableList(DiffRvItem.callback<T>()).also { it.update(newItems) }
|
||||
|
||||
fun <T : AnyDiffRvItem> filterableListOf() =
|
||||
FilterableDiffObservableList(DiffRvItem.callback<T>())
|
||||
|
||||
fun <T : RvItem> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
|
||||
override fun onBindBinding(
|
||||
binding: ViewDataBinding,
|
||||
variableId: Int,
|
||||
layoutRes: Int,
|
||||
position: Int,
|
||||
item: T
|
||||
) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
item.onBindingBound(binding)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T : RvItem> itemBindingOf(
|
||||
crossinline body: (ItemBinding<*>) -> Unit = {}
|
||||
) = OnItemBind<T> { itemBinding, _, item ->
|
||||
item.bind(itemBinding)
|
||||
body(itemBinding)
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ListChangeRegistry
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ObservableList.OnListChangedCallback
|
||||
import java.util.AbstractList
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
||||
|
||||
private val lists: MutableList<List<T>> = mutableListOf()
|
||||
private val listeners = ListChangeRegistry()
|
||||
private val callback = Callback<T>()
|
||||
|
||||
override fun addOnListChangedCallback(callback: OnListChangedCallback<out ObservableList<T>>) {
|
||||
listeners.add(callback)
|
||||
}
|
||||
|
||||
override fun removeOnListChangedCallback(callback: OnListChangedCallback<out ObservableList<T>>) {
|
||||
listeners.remove(callback)
|
||||
}
|
||||
|
||||
override fun get(index: Int): T {
|
||||
if (index < 0)
|
||||
throw IndexOutOfBoundsException()
|
||||
var idx = index
|
||||
for (list in lists) {
|
||||
val size = list.size
|
||||
if (idx < size) {
|
||||
return list[idx]
|
||||
}
|
||||
idx -= size
|
||||
}
|
||||
throw IndexOutOfBoundsException()
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = lists.fold(0) { i, it -> i + it.size }
|
||||
|
||||
|
||||
fun insertItem(obj: T): MergeObservableList<T> {
|
||||
val idx = size
|
||||
lists.add(listOf(obj))
|
||||
++modCount
|
||||
listeners.notifyInserted(this, idx, 1)
|
||||
return this
|
||||
}
|
||||
|
||||
fun insertList(list: List<T>): MergeObservableList<T> {
|
||||
val idx = size
|
||||
lists.add(list)
|
||||
++modCount
|
||||
(list as? ObservableList<T>)?.addOnListChangedCallback(callback)
|
||||
if (list.isNotEmpty())
|
||||
listeners.notifyInserted(this, idx, list.size)
|
||||
return this
|
||||
}
|
||||
|
||||
fun removeItem(obj: T): Boolean {
|
||||
var idx = 0
|
||||
for ((i, list) in lists.withIndex()) {
|
||||
if (list !is ObservableList<*>) {
|
||||
if (obj == list[0]) {
|
||||
lists.removeAt(i)
|
||||
++modCount
|
||||
listeners.notifyRemoved(this, idx, 1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
idx += list.size
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun removeList(listToRemove: List<T>): Boolean {
|
||||
var idx = 0
|
||||
for ((i, list) in lists.withIndex()) {
|
||||
if (listToRemove === list) {
|
||||
(list as? ObservableList<T>)?.removeOnListChangedCallback(callback)
|
||||
lists.removeAt(i)
|
||||
++modCount
|
||||
listeners.notifyRemoved(this, idx, list.size)
|
||||
return true
|
||||
}
|
||||
idx += list.size
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
val sz = size
|
||||
for (list in lists) {
|
||||
if (list is ObservableList) {
|
||||
list.removeOnListChangedCallback(callback)
|
||||
}
|
||||
}
|
||||
++modCount
|
||||
lists.clear()
|
||||
if (sz > 0)
|
||||
listeners.notifyRemoved(this, 0, sz)
|
||||
}
|
||||
|
||||
private fun subIndexToIndex(subList: List<*>, index: Int): Int {
|
||||
if (index < 0)
|
||||
throw IndexOutOfBoundsException()
|
||||
var idx = 0
|
||||
for (list in lists) {
|
||||
if (subList === list) {
|
||||
return idx + index
|
||||
}
|
||||
idx += list.size
|
||||
}
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
inner class Callback<T> : OnListChangedCallback<ObservableList<T>>() {
|
||||
override fun onChanged(sender: ObservableList<T>) {
|
||||
++modCount
|
||||
listeners.notifyChanged(this@MergeObservableList)
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
listeners.notifyChanged(this@MergeObservableList,
|
||||
subIndexToIndex(sender, positionStart), itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
++modCount
|
||||
listeners.notifyInserted(this@MergeObservableList,
|
||||
subIndexToIndex(sender, positionStart), itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(
|
||||
sender: ObservableList<T>,
|
||||
fromPosition: Int,
|
||||
toPosition: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
val idx = subIndexToIndex(sender, 0)
|
||||
listeners.notifyMoved(this@MergeObservableList,
|
||||
idx + fromPosition, idx + toPosition, itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
++modCount
|
||||
listeners.notifyRemoved(this@MergeObservableList,
|
||||
subIndexToIndex(sender, positionStart), itemCount)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,89 +1,35 @@
|
|||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.BR
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
|
||||
abstract class RvItem {
|
||||
|
||||
abstract val layoutRes: Int
|
||||
|
||||
@CallSuper
|
||||
open fun bind(binding: ItemBinding<*>) {
|
||||
binding.set(BR.item, layoutRes)
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback is useful if you want to manipulate your views directly.
|
||||
* If you want to use this callback, you must set [me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter]
|
||||
* on your RecyclerView and call it from there. You can use [BindingBoundAdapter] for your convenience.
|
||||
*/
|
||||
open fun onBindingBound(binding: ViewDataBinding) {}
|
||||
}
|
||||
|
||||
interface RvContainer<E> {
|
||||
val item: E
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* This item addresses issues where enclosing recycler has to be invalidated or generally
|
||||
* manipulated with. This shouldn't be however necessary for 99.9% of use-cases. Refrain from using
|
||||
* this item as it provides virtually no additional functionality. Stick with ComparableRvItem.
|
||||
* */
|
||||
|
||||
interface LenientRvItem {
|
||||
fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||
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
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue