mirror of https://github.com/topjohnwu/Magisk
Compare commits
923 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 |
|
@ -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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
paths:
|
paths:
|
||||||
- 'app/**'
|
- "app/**"
|
||||||
- 'native/**'
|
- "native/**"
|
||||||
- 'stub/**'
|
- "stub/**"
|
||||||
- 'buildSrc/**'
|
- "buildSrc/**"
|
||||||
- 'build.py'
|
- "build.py"
|
||||||
- 'gradle.properties'
|
- "gradle.properties"
|
||||||
- '.github/workflows/build.yml'
|
- ".github/workflows/build.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [master]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build on ${{ matrix.os }}
|
name: Build Magisk artifacts
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SCCACHE_DIRECT: false
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
|
||||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
|
||||||
env:
|
|
||||||
NDK_CCACHE: ${{ github.workspace }}/ccache
|
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
submodules: "recursive"
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Setup environment
|
||||||
uses: actions/setup-java@v1
|
uses: ./.github/actions/setup
|
||||||
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: Build release
|
- name: Build release
|
||||||
run: |
|
run: ./build.py -vr all
|
||||||
./ccache -zp
|
|
||||||
python build.py -vr all
|
|
||||||
|
|
||||||
- name: Build debug
|
- name: Build debug
|
||||||
run: |
|
run: ./build.py -v all
|
||||||
python build.py -v all
|
|
||||||
./ccache -s
|
|
||||||
|
|
||||||
- name: Stop gradle daemon
|
- name: Stop gradle daemon
|
||||||
run: ./gradlew --stop
|
run: ./gradlew --stop
|
||||||
|
|
||||||
# Only upload artifacts built on Linux
|
|
||||||
- name: Upload build artifact
|
- name: Upload build artifact
|
||||||
if: runner.os == 'Linux'
|
uses: actions/upload-artifact@v4
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
with:
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
path: out
|
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,42 +1,42 @@
|
||||||
[submodule "selinux"]
|
[submodule "selinux"]
|
||||||
path = native/jni/external/selinux
|
path = native/src/external/selinux
|
||||||
url = https://github.com/topjohnwu/selinux.git
|
url = https://github.com/topjohnwu/selinux.git
|
||||||
[submodule "busybox"]
|
[submodule "busybox"]
|
||||||
path = native/jni/external/busybox
|
path = native/src/external/busybox
|
||||||
url = https://github.com/topjohnwu/ndk-busybox.git
|
url = https://github.com/topjohnwu/ndk-busybox.git
|
||||||
[submodule "dtc"]
|
|
||||||
path = native/jni/external/dtc
|
|
||||||
url = https://github.com/dgibson/dtc.git
|
|
||||||
[submodule "lz4"]
|
[submodule "lz4"]
|
||||||
path = native/jni/external/lz4
|
path = native/src/external/lz4
|
||||||
url = https://github.com/lz4/lz4.git
|
url = https://github.com/lz4/lz4.git
|
||||||
[submodule "bzip2"]
|
[submodule "bzip2"]
|
||||||
path = native/jni/external/bzip2
|
path = native/src/external/bzip2
|
||||||
url = https://github.com/nemequ/bzip2.git
|
url = https://github.com/nemequ/bzip2.git
|
||||||
[submodule "xz"]
|
[submodule "xz"]
|
||||||
path = native/jni/external/xz
|
path = native/src/external/xz
|
||||||
url = https://github.com/xz-mirror/xz.git
|
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"]
|
[submodule "pcre"]
|
||||||
path = native/jni/external/pcre
|
path = native/src/external/pcre
|
||||||
url = https://android.googlesource.com/platform/external/pcre
|
url = https://android.googlesource.com/platform/external/pcre
|
||||||
[submodule "libcxx"]
|
[submodule "libcxx"]
|
||||||
path = native/jni/external/libcxx
|
path = native/src/external/libcxx
|
||||||
url = https://github.com/topjohnwu/libcxx.git
|
url = https://github.com/topjohnwu/libcxx.git
|
||||||
[submodule "zlib"]
|
[submodule "zlib"]
|
||||||
path = native/jni/external/zlib
|
path = native/src/external/zlib
|
||||||
url = https://android.googlesource.com/platform/external/zlib
|
url = https://android.googlesource.com/platform/external/zlib
|
||||||
[submodule "parallel-hashmap"]
|
[submodule "zopfli"]
|
||||||
path = native/jni/external/parallel-hashmap
|
path = native/src/external/zopfli
|
||||||
url = https://github.com/greg7mdp/parallel-hashmap.git
|
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"]
|
[submodule "termux-elf-cleaner"]
|
||||||
path = tools/termux-elf-cleaner
|
path = tools/termux-elf-cleaner
|
||||||
url = https://github.com/termux/termux-elf-cleaner.git
|
url = https://github.com/termux/termux-elf-cleaner.git
|
||||||
[submodule "zopfli"]
|
|
||||||
path = native/jni/external/zopfli
|
|
||||||
url = https://github.com/google/zopfli.git
|
|
||||||
|
|
32
README.MD
32
README.MD
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
## Introduction
|
## 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:
|
Some highlight features:
|
||||||
|
|
||||||
- **MagiskSU**: Provide root access for applications
|
- **MagiskSU**: Provide root access for applications
|
||||||
|
@ -18,16 +18,16 @@ Some highlight features:
|
||||||
|
|
||||||
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
[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.3-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v24.3)
|
[![](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-v25.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v25.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-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)
|
[![](https://img.shields.io/badge/Magisk-Debug-red)](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||||
|
|
||||||
## Useful Links
|
## Useful Links
|
||||||
|
|
||||||
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
- [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 Documentation](https://topjohnwu.github.io/Magisk/)
|
||||||
- [Magisk Troubleshoot Wiki](https://www.didgeridoohan.com/magisk/HomePage) (by [@Didgeridoohan](https://github.com/Didgeridoohan))
|
|
||||||
|
|
||||||
## Bug Reports
|
## Bug Reports
|
||||||
|
|
||||||
|
@ -37,30 +37,6 @@ For installation issues, upload both boot image and install logs.<br>
|
||||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||||
For Magisk app crashes, record and upload the logcat when the crash occurs.
|
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.
|
|
||||||
|
|
||||||
## Signing and Distribution
|
|
||||||
|
|
||||||
- The certificate of the key used to sign the final Magisk APK product is also directly embedded into some executables. In release builds, Magisk's root daemon will enforce this certificate check and reject and forcefully uninstall any non-matching Magisk apps to protect users from malicious and unverified Magisk APKs.
|
|
||||||
- To do any development on Magisk itself, switch to an **official debug build and reinstall Magisk** to bypass the signature check.
|
|
||||||
- To distribute your own Magisk builds signed with your own keys, set your signing configs in `config.prop`.
|
|
||||||
- Check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key) for more details on generating your own key.
|
|
||||||
|
|
||||||
## Translation Contributions
|
## Translation Contributions
|
||||||
|
|
||||||
Default string resources for the Magisk app and its stub APK are located here:
|
Default string resources for the Magisk app and its stub APK are located here:
|
||||||
|
|
|
@ -3,10 +3,9 @@
|
||||||
/local.properties
|
/local.properties
|
||||||
.idea/
|
.idea/
|
||||||
/build
|
/build
|
||||||
app/release
|
|
||||||
*.hprof
|
*.hprof
|
||||||
.externalNativeBuild/
|
.externalNativeBuild/
|
||||||
*.apk
|
*.apk
|
||||||
src/main/assets
|
src/*/assets
|
||||||
src/main/jniLibs
|
src/*/jniLibs
|
||||||
src/main/resources
|
src/*/resources
|
||||||
|
|
|
@ -26,7 +26,10 @@ android {
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
versionName = Config.version
|
versionName = Config.version
|
||||||
versionCode = Config.versionCode
|
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 {
|
buildTypes {
|
||||||
|
@ -39,11 +42,13 @@ android {
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
dataBinding = true
|
dataBinding = true
|
||||||
|
aidl = true
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
excludes += "/META-INF/*"
|
excludes += "/META-INF/*"
|
||||||
|
excludes += "/META-INF/versions/**"
|
||||||
excludes += "/org/bouncycastle/**"
|
excludes += "/org/bouncycastle/**"
|
||||||
excludes += "/kotlin/**"
|
excludes += "/kotlin/**"
|
||||||
excludes += "/kotlinx/**"
|
excludes += "/kotlinx/**"
|
||||||
|
@ -52,9 +57,6 @@ android {
|
||||||
excludes += "/*.bin"
|
excludes += "/*.bin"
|
||||||
excludes += "/*.json"
|
excludes += "/*.json"
|
||||||
}
|
}
|
||||||
jniLibs {
|
|
||||||
keepDebugSymbols += "**/*.so"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,17 +70,17 @@ configurations.all {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":app:shared"))
|
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:indeterminate-checkbox:1.0.7")
|
||||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
implementation("org.bouncycastle:bcpkix-jdk18on:1.71")
|
implementation("org.bouncycastle:bcpkix-jdk18on:1.77")
|
||||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
|
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
|
||||||
implementation("dev.rikka.rikkax.insets:insets:1.2.0")
|
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||||
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
|
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.2")
|
||||||
implementation("io.noties.markwon:core:4.6.2")
|
implementation("io.noties.markwon:core:4.6.2")
|
||||||
|
|
||||||
val vLibsu = "5.0.2"
|
val vLibsu = "5.2.2"
|
||||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||||
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
||||||
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
||||||
|
@ -88,33 +90,32 @@ dependencies {
|
||||||
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
||||||
implementation("com.squareup.retrofit2:converter-scalars:${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:okhttp:${vOkHttp}")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
||||||
|
|
||||||
val vMoshi = "1.13.0"
|
val vMoshi = "1.15.0"
|
||||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||||
|
|
||||||
val vRoom = "2.5.0-alpha02"
|
val vRoom = "2.6.1"
|
||||||
implementation("androidx.room:room-runtime:${vRoom}")
|
implementation("androidx.room:room-runtime:${vRoom}")
|
||||||
implementation("androidx.room:room-ktx:${vRoom}")
|
implementation("androidx.room:room-ktx:${vRoom}")
|
||||||
kapt("androidx.room:room-compiler:${vRoom}")
|
kapt("androidx.room:room-compiler:${vRoom}")
|
||||||
|
|
||||||
val vNav = "2.5.0-rc01"
|
val vNav = "2.7.7"
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||||
|
|
||||||
implementation("androidx.biometric:biometric:1.1.0")
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.appcompat:appcompat:1.4.2")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.preference:preference:1.2.0")
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
|
||||||
implementation("androidx.transition:transition:1.4.1")
|
implementation("androidx.transition:transition:1.4.1")
|
||||||
implementation("androidx.core:core-ktx:1.8.0")
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-rc01")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("com.google.android.material:material:1.6.1")
|
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
||||||
|
implementation("com.google.android.material:material:1.11.0")
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,15 @@
|
||||||
-assumenosideeffects class java.util.Objects {
|
-assumenosideeffects class java.util.Objects {
|
||||||
public static ** requireNonNull(...);
|
public static ** requireNonNull(...);
|
||||||
}
|
}
|
||||||
|
-assumenosideeffects public class kotlin.coroutines.jvm.internal.DebugMetadataKt {
|
||||||
|
private static ** getDebugMetadataAnnotation(...) return null;
|
||||||
|
}
|
||||||
|
|
||||||
# Stub
|
# Stub
|
||||||
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||||
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
|
||||||
boolean mActivityHandlesUiModeChecked;
|
boolean mActivityHandlesConfigFlagsChecked;
|
||||||
boolean mActivityHandlesUiMode;
|
int mActivityHandlesConfigFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
# main
|
# main
|
||||||
|
@ -30,6 +33,17 @@
|
||||||
public void d(**);
|
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
|
# Excessive obfuscation
|
||||||
-repackageclasses 'a'
|
-repackageclasses 'a'
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
|
|
|
@ -7,7 +7,3 @@ setupCommon()
|
||||||
android {
|
android {
|
||||||
namespace = "com.topjohnwu.shared"
|
namespace = "com.topjohnwu.shared"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api("io.michaelrocks:paranoid-core:0.3.7")
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.topjohnwu.shared"
|
|
||||||
android:installLocation="internalOnly">
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<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.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
<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.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
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="29"
|
android:maxSdkVersion="29"
|
||||||
|
|
|
@ -2,9 +2,6 @@ package com.topjohnwu.magisk;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public class ProviderInstaller {
|
public class ProviderInstaller {
|
||||||
|
|
||||||
public static boolean install(Context context) {
|
public static boolean install(Context context) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.topjohnwu.magisk;
|
||||||
import static android.os.Build.VERSION.SDK_INT;
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
|
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
@ -11,6 +12,7 @@ import android.content.res.AssetManager;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.content.res.loader.ResourcesLoader;
|
import android.content.res.loader.ResourcesLoader;
|
||||||
import android.content.res.loader.ResourcesProvider;
|
import android.content.res.loader.ResourcesProvider;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -18,9 +20,6 @@ import java.io.IOException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public class StubApk {
|
public class StubApk {
|
||||||
private static File dynDir;
|
private static File dynDir;
|
||||||
private static Method addAssetPath;
|
private static Method addAssetPath;
|
||||||
|
@ -28,7 +27,7 @@ public class StubApk {
|
||||||
private static File getDynDir(ApplicationInfo info) {
|
private static File getDynDir(ApplicationInfo info) {
|
||||||
if (dynDir == null) {
|
if (dynDir == null) {
|
||||||
final String dataDir;
|
final String dataDir;
|
||||||
if (SDK_INT >= 24) {
|
if (SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
// Use device protected path to allow directBootAware
|
// Use device protected path to allow directBootAware
|
||||||
dataDir = info.deviceProtectedDataDir;
|
dataDir = info.deviceProtectedDataDir;
|
||||||
} else {
|
} else {
|
||||||
|
@ -56,12 +55,24 @@ public class StubApk {
|
||||||
return new File(getDynDir(info), "update.apk");
|
return new File(getDynDir(info), "update.apk");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.R)
|
||||||
|
private static ResourcesLoader getResourcesLoader(File path) throws IOException {
|
||||||
|
var loader = new ResourcesLoader();
|
||||||
|
ResourcesProvider provider;
|
||||||
|
if (path.isDirectory()) {
|
||||||
|
provider = ResourcesProvider.loadFromDirectory(path.getPath(), null);
|
||||||
|
} else {
|
||||||
|
var fd = ParcelFileDescriptor.open(path, MODE_READ_ONLY);
|
||||||
|
provider = ResourcesProvider.loadFromApk(fd);
|
||||||
|
}
|
||||||
|
loader.addProvider(provider);
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
|
||||||
public static void addAssetPath(Resources res, String path) {
|
public static void addAssetPath(Resources res, String path) {
|
||||||
if (SDK_INT >= 30) {
|
if (SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
try (var fd = ParcelFileDescriptor.open(new File(path), MODE_READ_ONLY)) {
|
try {
|
||||||
var loader = new ResourcesLoader();
|
res.addLoaders(getResourcesLoader(new File(path)));
|
||||||
loader.addProvider(ResourcesProvider.loadFromApk(fd));
|
|
||||||
res.addLoaders(loader);
|
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
} else {
|
} else {
|
||||||
AssetManager asset = res.getAssets();
|
AssetManager asset = res.getAssets();
|
||||||
|
|
|
@ -25,9 +25,6 @@ import java.util.UUID;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.michaelrocks.paranoid.Obfuscate;
|
|
||||||
|
|
||||||
@Obfuscate
|
|
||||||
public final class APKInstall {
|
public final class APKInstall {
|
||||||
|
|
||||||
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
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) {
|
public static Session startSession(Context context) {
|
||||||
return startSession(context, null, null, null);
|
return startSession(context, null, null, null);
|
||||||
}
|
}
|
||||||
|
@ -51,17 +58,15 @@ public final class APKInstall {
|
||||||
// If pkg is not null, look for package added event
|
// If pkg is not null, look for package added event
|
||||||
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
||||||
filter.addDataScheme("package");
|
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;
|
return receiver;
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface Session {
|
public interface Session {
|
||||||
// @WorkerThread
|
// @WorkerThread
|
||||||
OutputStream openStream(Context context) throws IOException;
|
OutputStream openStream(Context context) throws IOException;
|
||||||
// @WorkerThread
|
|
||||||
void install(Context context, File apk) throws IOException;
|
|
||||||
// @WorkerThread @Nullable
|
// @WorkerThread @Nullable
|
||||||
Intent waitIntent();
|
Intent waitIntent();
|
||||||
}
|
}
|
||||||
|
@ -94,27 +99,25 @@ public final class APKInstall {
|
||||||
} else if (sessionId.equals(intent.getAction())) {
|
} else if (sessionId.equals(intent.getAction())) {
|
||||||
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case STATUS_PENDING_USER_ACTION:
|
case STATUS_PENDING_USER_ACTION ->
|
||||||
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||||
break;
|
case STATUS_SUCCESS -> {
|
||||||
case STATUS_SUCCESS:
|
|
||||||
if (packageName == null) {
|
if (packageName == null) {
|
||||||
onSuccess(context);
|
onSuccess(context);
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
default:
|
default -> {
|
||||||
int id = intent.getIntExtra(EXTRA_SESSION_ID, 0);
|
int id = intent.getIntExtra(EXTRA_SESSION_ID, 0);
|
||||||
if (id > 0) {
|
var installer = context.getPackageManager().getPackageInstaller();
|
||||||
var installer = context.getPackageManager().getPackageInstaller();
|
try {
|
||||||
var info = installer.getSessionInfo(id);
|
installer.abandonSession(id);
|
||||||
if (info != null) {
|
} catch (SecurityException ignored) {
|
||||||
installer.abandonSession(info.getSessionId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (onFailure != null) {
|
if (onFailure != null) {
|
||||||
onFailure.run();
|
onFailure.run();
|
||||||
}
|
}
|
||||||
context.getApplicationContext().unregisterReceiver(this);
|
context.getApplicationContext().unregisterReceiver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
latch.countDown();
|
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,5 +1,7 @@
|
||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import android.os.Process;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
@ -14,8 +16,8 @@ public class DynamicClassLoader extends BaseDexClassLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DynamicClassLoader(File apk, ClassLoader parent) {
|
public DynamicClassLoader(File apk, ClassLoader parent) {
|
||||||
// Set optimizedDirectory to null to bypass DexFile's security checks
|
// Set optimizedDirectory to null for RootService to bypass DexFile's security checks
|
||||||
super(apk.getPath(), null, null, parent);
|
super(apk.getPath(), Process.myUid() == 0 ? null : apk.getParentFile(), null, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2,12 +2,21 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<permission
|
||||||
|
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||||
|
android:protectionLevel="signature"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="${applicationId}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".core.App"
|
android:name=".core.App"
|
||||||
android:extractNativeLibs="true"
|
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:multiArch="true"
|
android:multiArch="true"
|
||||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||||
|
tools:remove="android:appComponentFactory">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
|
@ -52,8 +61,10 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.download.DownloadService"
|
android:name=".core.Service"
|
||||||
android:exported="false" />
|
android:exported="false"
|
||||||
|
android:enabled="@bool/enable_fg_service"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.JobService"
|
android:name=".core.JobService"
|
||||||
|
@ -72,11 +83,15 @@
|
||||||
android:name="androidx.room.MultiInstanceInvalidationService"
|
android:name="androidx.room.MultiInstanceInvalidationService"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
<!-- We don't need emoji compat -->
|
<!-- We handle initialization ourselves -->
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.startup.InitializationProvider"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
android:authorities="${applicationId}.androidx-startup"
|
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" />
|
tools:node="remove" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
|
@ -5,13 +5,14 @@ import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.OnRebindCallback
|
import androidx.databinding.OnRebindCallback
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.ktx.startAnimations
|
|
||||||
|
|
||||||
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
||||||
|
|
||||||
|
@ -37,6 +38,9 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||||
it.setVariable(BR.viewModel, viewModel)
|
it.setVariable(BR.viewModel, viewModel)
|
||||||
it.lifecycleOwner = viewLifecycleOwner
|
it.lifecycleOwner = viewLifecycleOwner
|
||||||
}
|
}
|
||||||
|
if (this is MenuProvider) {
|
||||||
|
activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
|
||||||
|
}
|
||||||
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
@ -89,5 +93,4 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||||
fun NavDirections.navigate() {
|
fun NavDirections.navigate() {
|
||||||
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
|
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
@ -8,11 +9,12 @@ import androidx.databinding.PropertyChangeRegistry
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.databinding.ObservableHost
|
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||||
import com.topjohnwu.magisk.events.BackPressEvent
|
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.NavigationEvent
|
||||||
import com.topjohnwu.magisk.events.PermissionEvent
|
import com.topjohnwu.magisk.events.PermissionEvent
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
|
@ -53,15 +55,25 @@ abstract class BaseViewModel : ViewModel(), ObservableHost {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 back() = BackPressEvent().publish()
|
||||||
|
|
||||||
fun <Event : ViewEvent> Event.publish() {
|
fun ViewEvent.publish() {
|
||||||
_viewEvents.postValue(this)
|
_viewEvents.postValue(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <Event : ViewEventWithScope> Event.publish() {
|
fun DialogBuilder.show() {
|
||||||
scope = viewModelScope
|
DialogEvent(this).publish()
|
||||||
_viewEvents.postValue(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavDirections.navigate(pop: Boolean = false) {
|
fun NavDirections.navigate(pop: Boolean = false) {
|
||||||
|
|
|
@ -20,12 +20,14 @@ abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Bindin
|
||||||
val navigation: NavController get() = navHostFragment.navController
|
val navigation: NavController get() = navHostFragment.navController
|
||||||
|
|
||||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
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() {
|
override fun onBackPressed() {
|
||||||
if (currentFragment?.onBackPressed()?.not() == true) {
|
if (binded) {
|
||||||
super.onBackPressed()
|
if (currentFragment?.onBackPressed() == false) {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,16 +5,20 @@ import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.res.use
|
import androidx.core.content.res.use
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.ViewDataBinding
|
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.google.android.material.snackbar.Snackbar
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.widget.Pre23CardViewBackgroundColorFixLayoutInflaterListener
|
|
||||||
import rikka.insets.WindowInsetsHelper
|
import rikka.insets.WindowInsetsHelper
|
||||||
import rikka.layoutinflater.view.LayoutInflaterFactory
|
import rikka.layoutinflater.view.LayoutInflaterFactory
|
||||||
|
|
||||||
|
@ -23,6 +27,8 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||||
protected lateinit var binding: Binding
|
protected lateinit var binding: Binding
|
||||||
protected abstract val layoutRes: Int
|
protected abstract val layoutRes: Int
|
||||||
|
|
||||||
|
protected val binded get() = ::binding.isInitialized
|
||||||
|
|
||||||
open val snackbarView get() = binding.root
|
open val snackbarView get() = binding.root
|
||||||
open val snackbarAnchorView: View? get() = null
|
open val snackbarAnchorView: View? get() = null
|
||||||
|
|
||||||
|
@ -33,11 +39,6 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
||||||
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
||||||
.apply {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
||||||
this.addOnViewCreatedListener(Pre23CardViewBackgroundColorFixLayoutInflaterListener.getInstance())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -101,3 +102,14 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||||
else -> Unit
|
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
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for passing events from ViewModels to Activities/Fragments
|
* Class for passing events from ViewModels to Activities/Fragments
|
||||||
|
@ -9,10 +8,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
*/
|
*/
|
||||||
abstract class ViewEvent
|
abstract class ViewEvent
|
||||||
|
|
||||||
abstract class ViewEventWithScope: ViewEvent() {
|
|
||||||
lateinit var scope: CoroutineScope
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextExecutor {
|
interface ContextExecutor {
|
||||||
operator fun invoke(context: Context)
|
operator fun invoke(context: Context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,8 @@ object VMFactory : ViewModelProvider.Factory {
|
||||||
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
||||||
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
||||||
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
||||||
InstallViewModel::class.java -> InstallViewModel(ServiceLocator.networkService)
|
InstallViewModel::class.java ->
|
||||||
|
InstallViewModel(ServiceLocator.networkService, ServiceLocator.markwon)
|
||||||
SuRequestViewModel::class.java ->
|
SuRequestViewModel::class.java ->
|
||||||
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
||||||
else -> modelClass.newInstance()
|
else -> modelClass.newInstance()
|
||||||
|
|
|
@ -5,14 +5,26 @@ import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
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.StubApk
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.utils.*
|
import com.topjohnwu.magisk.core.utils.DispatcherExecutor
|
||||||
|
import com.topjohnwu.magisk.core.utils.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.ui.surequest.SuRequestActivity
|
||||||
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.superuser.ipc.RootService
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
@ -35,6 +47,8 @@ open class App() : Application() {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Os.setenv("PATH", "${Os.getenv("PATH")}:/debug_ramdisk:/sbin", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(context: Context) {
|
override fun attachBaseContext(context: Context) {
|
||||||
|
@ -70,6 +84,18 @@ open class App() : Application() {
|
||||||
|
|
||||||
refreshLocale()
|
refreshLocale()
|
||||||
resources.patch()
|
resources.patch()
|
||||||
|
Notifications.setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
ProcessLifecycle.init(this)
|
||||||
|
NetworkObserver.init(this)
|
||||||
|
if (!BuildConfig.DEBUG && !isRunningAsStub) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
ProfileInstaller.writeProfile(this@App)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.util.Xml
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
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.BoolDBPropertyNoWrite
|
||||||
import com.topjohnwu.magisk.core.repository.DBConfig
|
import com.topjohnwu.magisk.core.repository.DBConfig
|
||||||
import com.topjohnwu.magisk.core.repository.PreferenceConfig
|
import com.topjohnwu.magisk.core.repository.PreferenceConfig
|
||||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.IOException
|
||||||
|
|
||||||
object Config : PreferenceConfig, DBConfig {
|
object Config : PreferenceConfig, DBConfig {
|
||||||
|
|
||||||
|
@ -24,13 +25,12 @@ object Config : PreferenceConfig, DBConfig {
|
||||||
override val context get() = ServiceLocator.deContext
|
override val context get() = ServiceLocator.deContext
|
||||||
override val coroutineScope get() = GlobalScope
|
override val coroutineScope get() = GlobalScope
|
||||||
|
|
||||||
@get:SuppressLint("ApplySharedPref")
|
private val prefsFile = File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||||
val prefsFile: File get() {
|
|
||||||
// Flush prefs to disk
|
@SuppressLint("ApplySharedPref")
|
||||||
prefs.edit().apply {
|
fun getPrefsFile(): File {
|
||||||
remove(Key.ASKED_HOME)
|
prefs.edit().remove(Key.ASKED_HOME).commit()
|
||||||
}.commit()
|
return prefsFile
|
||||||
return File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Key {
|
object Key {
|
||||||
|
@ -40,6 +40,7 @@ object Config : PreferenceConfig, DBConfig {
|
||||||
const val SU_MNT_NS = "mnt_ns"
|
const val SU_MNT_NS = "mnt_ns"
|
||||||
const val SU_BIOMETRIC = "su_biometric"
|
const val SU_BIOMETRIC = "su_biometric"
|
||||||
const val ZYGISK = "zygisk"
|
const val ZYGISK = "zygisk"
|
||||||
|
const val BOOTLOOP = "bootloop"
|
||||||
const val DENYLIST = "denylist"
|
const val DENYLIST = "denylist"
|
||||||
const val SU_MANAGER = "requester"
|
const val SU_MANAGER = "requester"
|
||||||
const val KEYSTORE = "keystore"
|
const val KEYSTORE = "keystore"
|
||||||
|
@ -117,7 +118,6 @@ object Config : PreferenceConfig, DBConfig {
|
||||||
|
|
||||||
@JvmField var keepVerity = false
|
@JvmField var keepVerity = false
|
||||||
@JvmField var keepEnc = false
|
@JvmField var keepEnc = false
|
||||||
@JvmField var patchVbmeta = false
|
|
||||||
@JvmField var recovery = false
|
@JvmField var recovery = false
|
||||||
|
|
||||||
var bootId by preference(Key.BOOT_ID, "")
|
var bootId by preference(Key.BOOT_ID, "")
|
||||||
|
@ -136,7 +136,15 @@ object Config : PreferenceConfig, DBConfig {
|
||||||
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
||||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||||
var suTapjack by preference(Key.SU_TAPJACK, true)
|
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 doh by preference(Key.DOH, false)
|
||||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||||
|
|
||||||
|
@ -152,8 +160,14 @@ object Config : PreferenceConfig, DBConfig {
|
||||||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
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 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 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 zygisk by dbSettings(Key.ZYGISK, false)
|
||||||
|
var bootloop by dbSettings(Key.BOOTLOOP, 0)
|
||||||
var denyList by BoolDBPropertyNoWrite(Key.DENYLIST, false)
|
var denyList by BoolDBPropertyNoWrite(Key.DENYLIST, false)
|
||||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||||
|
@ -162,10 +176,15 @@ object Config : PreferenceConfig, DBConfig {
|
||||||
|
|
||||||
fun load(pkg: String?) {
|
fun load(pkg: String?) {
|
||||||
// Only try to load prefs when fresh install and a previous package name is set
|
// Only try to load prefs when fresh install and a previous package name is set
|
||||||
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
if (pkg != null && prefs.all.isEmpty()) {
|
||||||
context.contentResolver.openInputStream(Provider.preferencesUri(pkg))?.use {
|
runBlocking {
|
||||||
prefs.edit { parsePrefs(it) }
|
try {
|
||||||
|
context.contentResolver
|
||||||
|
.openInputStream(Provider.preferencesUri(pkg))
|
||||||
|
?.writeTo(prefsFile, dispatcher = Dispatchers.Unconfined)
|
||||||
|
} catch (ignored: IOException) {}
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
|
@ -182,52 +201,4 @@ object Config : PreferenceConfig, DBConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
lateinit var MAGISKTMP: String
|
const val MAGISK_PATH = "/data/adb/modules"
|
||||||
val MAGISK_PATH get() = "$MAGISKTMP/modules"
|
|
||||||
const val TMPDIR = "/dev/tmp"
|
const val TMPDIR = "/dev/tmp"
|
||||||
const val MAGISK_LOG = "/cache/magisk.log"
|
const val MAGISK_LOG = "/cache/magisk.log"
|
||||||
|
|
||||||
|
@ -36,7 +35,8 @@ object Const {
|
||||||
}
|
}
|
||||||
|
|
||||||
object ID {
|
object ID {
|
||||||
const val JOB_SERVICE_ID = 7
|
const val DOWNLOAD_JOB_ID = 6
|
||||||
|
const val CHECK_UPDATE_JOB_ID = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
object Url {
|
object Url {
|
||||||
|
|
|
@ -13,8 +13,8 @@ import android.util.DisplayMetrics
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
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.core.utils.syncLocale
|
||||||
import com.topjohnwu.magisk.ktx.unwrap
|
|
||||||
|
|
||||||
lateinit var AppApkPath: String
|
lateinit var AppApkPath: String
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.os.Build
|
import android.app.KeyguardManager
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
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.model.UpdateInfo
|
||||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
|
|
||||||
import com.topjohnwu.magisk.ktx.getProperty
|
|
||||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||||
|
|
||||||
val isRunningAsStub get() = Info.stub != null
|
val isRunningAsStub get() = Info.stub != null
|
||||||
|
@ -28,30 +26,31 @@ object Info {
|
||||||
// Device state
|
// Device state
|
||||||
@JvmStatic val env by lazy { loadState() }
|
@JvmStatic val env by lazy { loadState() }
|
||||||
@JvmField var isSAR = false
|
@JvmField var isSAR = false
|
||||||
|
var legacySAR = false
|
||||||
var isAB = false
|
var isAB = false
|
||||||
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
||||||
@JvmStatic val isFDE get() = crypto == "block"
|
@JvmStatic val isFDE get() = crypto == "block"
|
||||||
@JvmField var ramdisk = false
|
@JvmField var ramdisk = false
|
||||||
@JvmField var vbmeta = false
|
var patchBootVbmeta = false
|
||||||
var crypto = ""
|
var crypto = ""
|
||||||
var noDataExec = false
|
var noDataExec = false
|
||||||
var isRooted = false
|
var isRooted = false
|
||||||
|
|
||||||
@JvmField var hasGMS = true
|
@JvmField var hasGMS = true
|
||||||
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
|
||||||
@JvmField val isEmulator =
|
@JvmField val isEmulator =
|
||||||
getProperty("ro.kernel.qemu", "0") == "1" ||
|
getProperty("ro.kernel.qemu", "0") == "1" ||
|
||||||
getProperty("ro.boot.qemu", "0") == "1"
|
getProperty("ro.boot.qemu", "0") == "1"
|
||||||
|
|
||||||
val isConnected: LiveData<Boolean> by lazy {
|
val isConnected = MutableLiveData(false)
|
||||||
MutableLiveData(false).also { field ->
|
|
||||||
NetworkObserver.observe(AppContext) {
|
val showSuperUser: Boolean get() {
|
||||||
remote = EMPTY_REMOTE
|
return env.isActive && (Const.USER_ID == 0
|
||||||
field.postValue(it)
|
|| Config.suMultiuserMode == Config.Value.MULTIUSER_MODE_USER)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isDeviceSecure get() =
|
||||||
|
AppContext.getSystemService(KeyguardManager::class.java).isDeviceSecure
|
||||||
|
|
||||||
private fun loadState(): Env {
|
private fun loadState(): Env {
|
||||||
val v = fastCmd("magisk -v").split(":".toRegex())
|
val v = fastCmd("magisk -v").split(":".toRegex())
|
||||||
return Env(
|
return Env(
|
||||||
|
@ -67,9 +66,10 @@ object Info {
|
||||||
) {
|
) {
|
||||||
val versionCode = when {
|
val versionCode = when {
|
||||||
code < Const.Version.MIN_VERCODE -> -1
|
code < Const.Version.MIN_VERCODE -> -1
|
||||||
else -> if (isRooted) code else -1
|
isRooted -> code
|
||||||
|
else -> -1
|
||||||
}
|
}
|
||||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
val 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
|
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.JobInfo
|
||||||
import android.app.job.JobParameters
|
import android.app.job.JobParameters
|
||||||
import android.app.job.JobScheduler
|
import android.app.job.JobScheduler
|
||||||
|
@ -8,38 +11,78 @@ import androidx.core.content.getSystemService
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.core.base.BaseJobService
|
import com.topjohnwu.magisk.core.base.BaseJobService
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||||
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class JobService : BaseJobService() {
|
class JobService : BaseJobService() {
|
||||||
|
|
||||||
private val job = Job()
|
private var mSession: Session? = null
|
||||||
private val svc get() = ServiceLocator.networkService
|
|
||||||
|
|
||||||
override fun onStartJob(params: JobParameters): Boolean {
|
@TargetApi(value = 34)
|
||||||
val coroutineScope = CoroutineScope(Dispatchers.IO + job)
|
inner class Session(
|
||||||
coroutineScope.launch {
|
private var params: JobParameters
|
||||||
doWork()
|
) : 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)
|
jobFinished(params, false)
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doWork() {
|
@SuppressLint("NewApi")
|
||||||
svc.fetchUpdate()?.let {
|
override fun onStartJob(params: JobParameters): Boolean {
|
||||||
Info.remote = it
|
return when (params.jobId) {
|
||||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
Const.ID.CHECK_UPDATE_JOB_ID -> checkUpdate(params)
|
||||||
Notifications.updateAvailable(this)
|
Const.ID.DOWNLOAD_JOB_ID -> downloadFile(params)
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopJob(params: JobParameters): Boolean {
|
override fun onStopJob(params: JobParameters?) = false
|
||||||
job.cancel()
|
|
||||||
return 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 {
|
companion object {
|
||||||
|
@ -47,14 +90,14 @@ class JobService : BaseJobService() {
|
||||||
val scheduler = context.getSystemService<JobScheduler>() ?: return
|
val scheduler = context.getSystemService<JobScheduler>() ?: return
|
||||||
if (Config.checkUpdate) {
|
if (Config.checkUpdate) {
|
||||||
val cmp = JobService::class.java.cmp(context.packageName)
|
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))
|
.setPeriodic(TimeUnit.HOURS.toMillis(12))
|
||||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||||
.setRequiresDeviceIdle(true)
|
.setRequiresDeviceIdle(true)
|
||||||
.build()
|
.build()
|
||||||
scheduler.schedule(info)
|
scheduler.schedule(info)
|
||||||
} else {
|
} else {
|
||||||
scheduler.cancel(Const.ID.JOB_SERVICE_ID)
|
scheduler.cancel(Const.ID.CHECK_UPDATE_JOB_ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,17 +6,23 @@ import android.os.ParcelFileDescriptor
|
||||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||||
import com.topjohnwu.magisk.core.base.BaseProvider
|
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||||
|
import com.topjohnwu.magisk.core.su.TestHandler
|
||||||
|
|
||||||
class Provider : BaseProvider() {
|
class Provider : BaseProvider() {
|
||||||
|
|
||||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||||
SuCallbackHandler.run(context!!, method, extras)
|
return when (method) {
|
||||||
return Bundle.EMPTY
|
SuCallbackHandler.LOG, SuCallbackHandler.NOTIFY -> {
|
||||||
|
SuCallbackHandler.run(context!!, method, extras)
|
||||||
|
Bundle.EMPTY
|
||||||
|
}
|
||||||
|
else -> TestHandler.run(method)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||||
return when (uri.encodedPath ?: return null) {
|
return when (uri.encodedPath ?: return null) {
|
||||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
"/prefs_file" -> ParcelFileDescriptor.open(Config.getPrefsFile(), MODE_READ_ONLY)
|
||||||
else -> super.openFile(uri, mode)
|
else -> super.openFile(uri, mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,11 @@ package com.topjohnwu.magisk.core
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.core.content.IntentCompat
|
||||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||||
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
@ -35,6 +38,12 @@ open class Receiver : BaseReceiver() {
|
||||||
}
|
}
|
||||||
|
|
||||||
when (intent.action ?: return) {
|
when (intent.action ?: return) {
|
||||||
|
DownloadEngine.ACTION -> {
|
||||||
|
IntentCompat.getParcelableExtra(
|
||||||
|
intent, DownloadEngine.SUBJECT_KEY, Subject::class.java)?.let {
|
||||||
|
DownloadEngine.start(context, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||||
// This will only work pre-O
|
// This will only work pre-O
|
||||||
if (Config.suReAuth)
|
if (Config.suReAuth)
|
||||||
|
@ -51,7 +60,7 @@ open class Receiver : BaseReceiver() {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
||||||
if (installer == 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,5 +1,6 @@
|
||||||
package com.topjohnwu.magisk.core.base
|
package com.topjohnwu.magisk.core.base
|
||||||
|
|
||||||
|
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
@ -17,10 +18,11 @@ import androidx.activity.result.contract.ActivityResultContracts.RequestPermissi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
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.RequestInstall
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import com.topjohnwu.magisk.core.wrap
|
||||||
import com.topjohnwu.magisk.ktx.reflectField
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
|
|
||||||
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
||||||
fun onActivityLaunch() {}
|
fun onActivityLaunch() {}
|
||||||
|
@ -35,9 +37,17 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||||
permissionCallback?.invoke(it)
|
permissionCallback?.invoke(it)
|
||||||
permissionCallback = null
|
permissionCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var installCallback: ((Boolean) -> Unit)? = null
|
||||||
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
||||||
permissionCallback?.invoke(it)
|
installCallback?.invoke(it)
|
||||||
permissionCallback = null
|
installCallback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
var authenticateCallback: ((Boolean) -> Unit)? = null
|
||||||
|
val requestAuthenticate = registerForActivityResult(RequestAuthentication()) {
|
||||||
|
authenticateCallback?.invoke(it)
|
||||||
|
authenticateCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private var contentCallback: ContentResultCallback? = null
|
private var contentCallback: ContentResultCallback? = null
|
||||||
|
@ -52,9 +62,7 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
|
||||||
val realCallingPackage: String? get() {
|
val realCallingPackage: String? get() {
|
||||||
callingPackage?.let { return it }
|
callingPackage?.let { return it }
|
||||||
if (Build.VERSION.SDK_INT >= 22) {
|
mReferrerField.get(this)?.let { return it as String }
|
||||||
mReferrerField.get(this)?.let { return it as String }
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,8 +75,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||||
// Overwrite private members to avoid nasty "false" stack traces being logged
|
// Overwrite private members to avoid nasty "false" stack traces being logged
|
||||||
val delegate = delegate
|
val delegate = delegate
|
||||||
val clz = delegate.javaClass
|
val clz = delegate.javaClass
|
||||||
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
|
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
|
||||||
clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
|
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
|
||||||
}
|
}
|
||||||
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -82,15 +90,23 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||||
// We do not need external rw on 30+
|
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)
|
callback(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
permissionCallback = callback
|
|
||||||
if (permission == REQUEST_INSTALL_PACKAGES) {
|
if (permission == REQUEST_INSTALL_PACKAGES) {
|
||||||
|
installCallback = callback
|
||||||
requestInstall.launch(Unit)
|
requestInstall.launch(Unit)
|
||||||
} else {
|
} else {
|
||||||
|
permissionCallback = callback
|
||||||
requestPermission.launch(permission)
|
requestPermission.launch(permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,7 +117,7 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||||
getContent.launch(type)
|
getContent.launch(type)
|
||||||
callback.onActivityLaunch()
|
callback.onActivityLaunch()
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,11 @@ import com.topjohnwu.magisk.core.model.BranchInfo
|
||||||
import com.topjohnwu.magisk.core.model.ModuleJson
|
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||||
import okhttp3.ResponseBody
|
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 BRANCH = "branch"
|
||||||
private const val REPO = "repo"
|
private const val REPO = "repo"
|
||||||
|
@ -12,15 +16,12 @@ private const val FILE = "file"
|
||||||
|
|
||||||
interface GithubPageServices {
|
interface GithubPageServices {
|
||||||
|
|
||||||
@GET("{$FILE}")
|
@GET
|
||||||
suspend fun fetchUpdateJSON(@Path(FILE) file: String): UpdateInfo
|
suspend fun fetchUpdateJSON(@Url file: String): UpdateInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RawServices {
|
interface RawServices {
|
||||||
|
|
||||||
@GET
|
|
||||||
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Streaming
|
@Streaming
|
||||||
suspend fun fetchFile(@Url url: String): ResponseBody
|
suspend fun fetchFile(@Url url: String): ResponseBody
|
||||||
|
|
|
@ -1,15 +1,31 @@
|
||||||
package com.topjohnwu.magisk.core.data
|
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 com.topjohnwu.magisk.core.model.su.SuLog
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
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 class SuLogDatabase : RoomDatabase() {
|
||||||
|
|
||||||
abstract fun suLogDao(): SuLogDao
|
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
|
@Dao
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.topjohnwu.magisk.core.data.magiskdb
|
package com.topjohnwu.magisk.core.data.magiskdb
|
||||||
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
import com.topjohnwu.magisk.core.ktx.await
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
|
@ -9,9 +9,9 @@ import com.topjohnwu.magisk.core.data.SuLogDatabase
|
||||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||||
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
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.LogRepository
|
||||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||||
|
|
||||||
|
@ -39,13 +39,13 @@ object ServiceLocator {
|
||||||
NetworkService(
|
NetworkService(
|
||||||
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
|
||||||
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
|
||||||
createApiService(retrofit, Const.Url.GITHUB_API_URL)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSuLogDatabase(context: Context) =
|
private fun createSuLogDatabase(context: Context) =
|
||||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||||
|
.addMigrations(SuLogDatabase.MIGRATION_1_2)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.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,218 +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.ActivityTracker
|
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.core.intent
|
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
|
||||||
import com.topjohnwu.magisk.ktx.copyAndClose
|
|
||||||
import com.topjohnwu.magisk.ktx.forEach
|
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.magisk.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,110 +0,0 @@
|
||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
|
||||||
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 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,22 +9,16 @@ import android.os.Parcelable
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
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.MagiskJson
|
||||||
import com.topjohnwu.magisk.core.model.StubJson
|
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.io.File
|
||||||
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri()
|
import java.util.UUID
|
||||||
|
|
||||||
enum class Action {
|
|
||||||
Flash,
|
|
||||||
Download
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class Subject : Parcelable {
|
sealed class Subject : Parcelable {
|
||||||
|
|
||||||
|
@ -33,19 +27,17 @@ sealed class Subject : Parcelable {
|
||||||
abstract val title: String
|
abstract val title: String
|
||||||
abstract val notifyId: Int
|
abstract val notifyId: Int
|
||||||
open val autoLaunch: Boolean get() = true
|
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
|
@Parcelize
|
||||||
class Module(
|
class Module(
|
||||||
val module: OnlineModule,
|
private val module: OnlineModule,
|
||||||
val action: Action,
|
override val autoLaunch: Boolean,
|
||||||
override val notifyId: Int = Notifications.nextId()
|
override val notifyId: Int = Notifications.nextId()
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val url: String get() = module.zipUrl
|
override val url: String get() = module.zipUrl
|
||||||
override val title: String get() = module.downloadFilename
|
override val title: String get() = module.downloadFilename
|
||||||
override val autoLaunch: Boolean get() = action == Action.Flash
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
|
@ -59,7 +51,6 @@ sealed class Subject : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class App(
|
class App(
|
||||||
private val json: MagiskJson = Info.remote.magisk,
|
private val json: MagiskJson = Info.remote.magisk,
|
||||||
val stub: StubJson = Info.remote.stub,
|
|
||||||
override val notifyId: Int = Notifications.nextId()
|
override val notifyId: Int = Notifications.nextId()
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||||
|
@ -67,17 +58,24 @@ sealed class Subject : Parcelable {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
cachedFile("manager.apk")
|
MediaStoreUtils.getFile("${title}.apk").uri
|
||||||
}
|
}
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override var postDownload: (() -> Unit)? = null
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
var intent: Intent? = null
|
var intent: Intent? = null
|
||||||
override fun pendingIntent(context: Context) = intent?.toPending(context)
|
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")
|
@SuppressLint("InlinedApi")
|
||||||
protected fun Intent.toPending(context: Context): PendingIntent {
|
protected fun Intent.toPending(context: Context): PendingIntent {
|
||||||
return PendingIntent.getActivity(context, notifyId, this,
|
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() }
|
||||||
|
}
|
|
@ -1,17 +1,22 @@
|
||||||
package com.topjohnwu.magisk.ktx
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
import androidx.collection.SparseArrayCompat
|
import androidx.collection.SparseArrayCompat
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flatMapMerge
|
import kotlinx.coroutines.flow.flatMapMerge
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Collections
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
|
@ -35,9 +40,38 @@ inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun InputStream.copyAndClose(out: OutputStream) = withStreams(this, out) { i, o -> i.copyTo(o) }
|
@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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun InputStream.writeTo(file: File) = copyAndClose(file.outputStream())
|
@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) {
|
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
|
||||||
put(key, value)
|
put(key, value)
|
|
@ -1,8 +1,6 @@
|
||||||
package com.topjohnwu.magisk.ktx
|
package com.topjohnwu.magisk.core.ktx
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -15,12 +13,4 @@ fun reboot(reason: String = if (Config.recovery) "recovery" else "") {
|
||||||
Shell.cmd("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
Shell.cmd("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun relaunchApp(context: Context) {
|
|
||||||
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return
|
|
||||||
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
|
||||||
val cmd = intent.toCommand(args).joinToString(separator = " ")
|
|
||||||
Shell.cmd("run_delay 1 \"$cmd\"").exec()
|
|
||||||
Runtime.getRuntime().exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun Shell.Job.await() = withContext(Dispatchers.IO) { exec() }
|
suspend fun Shell.Job.await() = withContext(Dispatchers.IO) { exec() }
|
|
@ -7,7 +7,6 @@ import kotlinx.parcelize.Parcelize
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class UpdateInfo(
|
data class UpdateInfo(
|
||||||
val magisk: MagiskJson = MagiskJson(),
|
val magisk: MagiskJson = MagiskJson(),
|
||||||
val stub: StubJson = StubJson()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@ -19,13 +18,6 @@ data class MagiskJson(
|
||||||
val note: String = ""
|
val note: String = ""
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class StubJson(
|
|
||||||
val versionCode: Int = -1,
|
|
||||||
val link: String = ""
|
|
||||||
) : Parcelable
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ModuleJson(
|
data class ModuleJson(
|
||||||
val version: String,
|
val version: String,
|
||||||
|
|
|
@ -43,10 +43,10 @@ data class LocalModule(
|
||||||
set(enable) {
|
set(enable) {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
disableFile.delete()
|
disableFile.delete()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
} else {
|
} else {
|
||||||
!disableFile.createNewFile()
|
!disableFile.createNewFile()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,10 +56,10 @@ data class LocalModule(
|
||||||
if (remove) {
|
if (remove) {
|
||||||
if (updateFile.exists()) return
|
if (updateFile.exists()) return
|
||||||
removeFile.createNewFile()
|
removeFile.createNewFile()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
} else {
|
} else {
|
||||||
removeFile.delete()
|
removeFile.delete()
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_preinit_files").submit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,15 +122,13 @@ data class LocalModule(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
|
||||||
|
|
||||||
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
||||||
|
|
||||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||||
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
||||||
.listFiles()
|
.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.filter { !it.isFile }
|
.filter { !it.isFile && !it.isHidden }
|
||||||
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
||||||
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
.sortedBy { it.name.lowercase(Locale.ROOT) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.topjohnwu.magisk.ktx.getLabel
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
|
|
||||||
@Entity(tableName = "logs")
|
@Entity(tableName = "logs")
|
||||||
class SuLog(
|
class SuLog(
|
||||||
|
@ -14,7 +14,10 @@ class SuLog(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val appName: String,
|
val appName: String,
|
||||||
val command: String,
|
val command: String,
|
||||||
val action: Boolean,
|
val action: Int,
|
||||||
|
val target: Int,
|
||||||
|
val context: String,
|
||||||
|
val gids: String,
|
||||||
val time: Long = System.currentTimeMillis()
|
val time: Long = System.currentTimeMillis()
|
||||||
) {
|
) {
|
||||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||||
|
@ -25,7 +28,10 @@ fun PackageManager.createSuLog(
|
||||||
toUid: Int,
|
toUid: Int,
|
||||||
fromPid: Int,
|
fromPid: Int,
|
||||||
command: String,
|
command: String,
|
||||||
policy: Int
|
policy: Int,
|
||||||
|
target: Int,
|
||||||
|
context: String,
|
||||||
|
gids: String,
|
||||||
): SuLog {
|
): SuLog {
|
||||||
val appInfo = info.applicationInfo
|
val appInfo = info.applicationInfo
|
||||||
return SuLog(
|
return SuLog(
|
||||||
|
@ -35,7 +41,10 @@ fun PackageManager.createSuLog(
|
||||||
packageName = getNameForUid(appInfo.uid)!!,
|
packageName = getNameForUid(appInfo.uid)!!,
|
||||||
appName = appInfo.getLabel(this),
|
appName = appInfo.getLabel(this),
|
||||||
command = command,
|
command = command,
|
||||||
action = policy == SuPolicy.ALLOW
|
action = policy,
|
||||||
|
target = target,
|
||||||
|
context = context,
|
||||||
|
gids = gids,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +53,10 @@ fun createSuLog(
|
||||||
toUid: Int,
|
toUid: Int,
|
||||||
fromPid: Int,
|
fromPid: Int,
|
||||||
command: String,
|
command: String,
|
||||||
policy: Int
|
policy: Int,
|
||||||
|
target: Int,
|
||||||
|
context: String,
|
||||||
|
gids: String,
|
||||||
): SuLog {
|
): SuLog {
|
||||||
return SuLog(
|
return SuLog(
|
||||||
fromUid = fromUid,
|
fromUid = fromUid,
|
||||||
|
@ -53,6 +65,9 @@ fun createSuLog(
|
||||||
packageName = "[UID] $fromUid",
|
packageName = "[UID] $fromUid",
|
||||||
appName = "[UID] $fromUid",
|
appName = "[UID] $fromUid",
|
||||||
command = command,
|
command = command,
|
||||||
action = policy == SuPolicy.ALLOW
|
action = policy,
|
||||||
|
target = target,
|
||||||
|
context = context,
|
||||||
|
gids = gids,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ package com.topjohnwu.magisk.core.repository
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.data.SuLogDao
|
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.core.model.su.SuLog
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import com.topjohnwu.magisk.core.Config.Value.DEBUG_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.data.GithubApiServices
|
|
||||||
import com.topjohnwu.magisk.core.data.GithubPageServices
|
import com.topjohnwu.magisk.core.data.GithubPageServices
|
||||||
import com.topjohnwu.magisk.core.data.RawServices
|
import com.topjohnwu.magisk.core.data.RawServices
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
@ -17,8 +16,7 @@ import java.io.IOException
|
||||||
|
|
||||||
class NetworkService(
|
class NetworkService(
|
||||||
private val pages: GithubPageServices,
|
private val pages: GithubPageServices,
|
||||||
private val raw: RawServices,
|
private val raw: RawServices
|
||||||
private val api: GithubApiServices
|
|
||||||
) {
|
) {
|
||||||
suspend fun fetchUpdate() = safe {
|
suspend fun fetchUpdate() = safe {
|
||||||
var info = when (Config.updateChannel) {
|
var info = when (Config.updateChannel) {
|
||||||
|
@ -42,7 +40,7 @@ class NetworkService(
|
||||||
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
||||||
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
||||||
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
|
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
|
||||||
private suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
|
private suspend fun fetchCustomUpdate(url: String) = pages.fetchUpdateJSON(url)
|
||||||
|
|
||||||
private inline fun <T> safe(factory: () -> T): T? {
|
private inline fun <T> safe(factory: () -> T): T? {
|
||||||
return try {
|
return try {
|
||||||
|
|
|
@ -7,11 +7,11 @@ import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
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.SuPolicy
|
||||||
import com.topjohnwu.magisk.core.model.su.createSuLog
|
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||||
import com.topjohnwu.magisk.ktx.getLabel
|
|
||||||
import com.topjohnwu.magisk.ktx.getPackageInfo
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@ -57,17 +57,20 @@ object SuCallbackHandler {
|
||||||
val toUid = data.getIntComp("to.uid", -1)
|
val toUid = data.getIntComp("to.uid", -1)
|
||||||
val pid = data.getIntComp("pid", -1)
|
val pid = data.getIntComp("pid", -1)
|
||||||
val command = data.getString("command", "")
|
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 pm = context.packageManager
|
||||||
|
|
||||||
val log = runCatching {
|
val log = runCatching {
|
||||||
pm.getPackageInfo(fromUid, pid)?.let {
|
pm.getPackageInfo(fromUid, pid)?.let {
|
||||||
pm.createSuLog(it, toUid, pid, command, policy)
|
pm.createSuLog(it, toUid, pid, command, policy, target, seContext, gids)
|
||||||
}
|
}
|
||||||
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy)
|
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy, target, seContext, gids)
|
||||||
|
|
||||||
if (notify)
|
if (notify)
|
||||||
notify(context, log.action, log.appName)
|
notify(context, log.action == SuPolicy.ALLOW, log.appName)
|
||||||
|
|
||||||
runBlocking { ServiceLocator.logRepo.insert(log) }
|
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||||
}
|
}
|
||||||
|
@ -93,7 +96,7 @@ object SuCallbackHandler {
|
||||||
else
|
else
|
||||||
R.string.su_deny_toast
|
R.string.su_deny_toast
|
||||||
|
|
||||||
Utils.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
context.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,14 @@ import android.content.pm.PackageManager
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.data.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.SuPolicy
|
||||||
import com.topjohnwu.magisk.ktx.getPackageInfo
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -22,7 +23,7 @@ class SuRequestHandler(
|
||||||
private val policyDB: PolicyDao
|
private val policyDB: PolicyDao
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private lateinit var output: DataOutputStream
|
private lateinit var output: File
|
||||||
private lateinit var policy: SuPolicy
|
private lateinit var policy: SuPolicy
|
||||||
lateinit var pkgInfo: PackageInfo
|
lateinit var pkgInfo: PackageInfo
|
||||||
private set
|
private set
|
||||||
|
@ -52,37 +53,32 @@ class SuRequestHandler(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun close() {
|
private suspend fun init(intent: Intent): Boolean {
|
||||||
if (::output.isInitialized)
|
val uid = intent.getIntExtra("uid", -1)
|
||||||
runCatching { output.close() }
|
val pid = intent.getIntExtra("pid", -1)
|
||||||
}
|
val fifo = intent.getStringExtra("fifo")
|
||||||
|
if (uid <= 0 || pid <= 0 || fifo == null) {
|
||||||
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
|
Timber.e("Unexpected extras: uid=[${uid}], pid=[${pid}], fifo=[${fifo}]")
|
||||||
try {
|
return false
|
||||||
val fifo = intent.getStringExtra("fifo") ?: throw IOException("fifo == null")
|
|
||||||
output = DataOutputStream(FileOutputStream(fifo))
|
|
||||||
val uid = intent.getIntExtra("uid", -1)
|
|
||||||
if (uid <= 0) {
|
|
||||||
throw IOException("uid == $uid")
|
|
||||||
}
|
|
||||||
policy = SuPolicy(uid)
|
|
||||||
val pid = intent.getIntExtra("pid", -1)
|
|
||||||
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]
|
|
||||||
}
|
|
||||||
return@withContext true
|
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
|
||||||
respond(SuPolicy.DENY, -1)
|
|
||||||
return@withContext false
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
close()
|
|
||||||
return@withContext 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
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun respond(action: Int, time: Int) {
|
suspend fun respond(action: Int, time: Int) {
|
||||||
|
@ -97,14 +93,15 @@ class SuRequestHandler(
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
output.writeInt(policy.policy)
|
DataOutputStream(FileOutputStream(output)).use {
|
||||||
output.flush()
|
it.writeInt(policy.policy)
|
||||||
|
it.flush()
|
||||||
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
} finally {
|
}
|
||||||
close()
|
if (until >= 0) {
|
||||||
if (until >= 0)
|
policyDB.update(policy)
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,10 +4,10 @@ import android.net.Uri
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
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.displayName
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.unzip
|
import com.topjohnwu.magisk.core.utils.unzip
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -26,7 +26,7 @@ open class FlashZip(
|
||||||
private lateinit var zipFile: File
|
private lateinit var zipFile: File
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun flash(): Boolean {
|
private suspend fun flash(): Boolean {
|
||||||
installDir.deleteRecursively()
|
installDir.deleteRecursively()
|
||||||
installDir.mkdirs()
|
installDir.mkdirs()
|
||||||
|
|
||||||
|
@ -47,13 +47,13 @@ open class FlashZip(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isValid = runCatching {
|
val isValid = try {
|
||||||
zipFile.unzip(installDir, "META-INF/com/google/android", true)
|
zipFile.unzip(installDir, "META-INF/com/google/android", true)
|
||||||
val script = File(installDir, "updater-script")
|
val script = File(installDir, "updater-script")
|
||||||
script.readText().contains("#MAGISK")
|
script.readText().contains("#MAGISK")
|
||||||
}.getOrElse {
|
} catch (e: IOException) {
|
||||||
console.add("! Unzip error")
|
console.add("! Unzip error")
|
||||||
throw it
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
|
|
@ -4,22 +4,22 @@ import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.core.Provider
|
import com.topjohnwu.magisk.core.Provider
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
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.AXML
|
||||||
import com.topjohnwu.magisk.core.utils.Keygen
|
import com.topjohnwu.magisk.core.utils.Keygen
|
||||||
import com.topjohnwu.magisk.ktx.await
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
|
||||||
import com.topjohnwu.magisk.signing.JarMap
|
import com.topjohnwu.magisk.signing.JarMap
|
||||||
import com.topjohnwu.magisk.signing.SignApk
|
import com.topjohnwu.magisk.signing.SignApk
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Runnable
|
import kotlinx.coroutines.Runnable
|
||||||
|
@ -30,6 +30,7 @@ import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
import kotlin.random.asKotlinRandom
|
||||||
|
|
||||||
object HideAPK {
|
object HideAPK {
|
||||||
|
|
||||||
|
@ -39,8 +40,7 @@ object HideAPK {
|
||||||
|
|
||||||
// Some arbitrary limit
|
// Some arbitrary limit
|
||||||
const val MAX_LABEL_LENGTH = 32
|
const val MAX_LABEL_LENGTH = 32
|
||||||
|
const val PLACEHOLDER = "COMPONENT_PLACEHOLDER"
|
||||||
private val svc get() = ServiceLocator.networkService
|
|
||||||
|
|
||||||
private fun genPackageName(): String {
|
private fun genPackageName(): String {
|
||||||
val random = SecureRandom()
|
val random = SecureRandom()
|
||||||
|
@ -65,20 +65,87 @@ object HideAPK {
|
||||||
return builder.toString()
|
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,
|
context: Context,
|
||||||
apk: File, out: OutputStream,
|
apk: File, out: OutputStream,
|
||||||
pkg: String, label: CharSequence
|
pkg: String, label: CharSequence
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
|
val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
|
||||||
val name = info.applicationInfo.nonLocalizedLabel.toString()
|
val origLabel = info.applicationInfo.nonLocalizedLabel.toString()
|
||||||
try {
|
try {
|
||||||
JarMap.open(apk, true).use { jar ->
|
JarMap.open(apk, true).use { jar ->
|
||||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||||
val xml = AXML(jar.getRawData(je))
|
val xml = AXML(jar.getRawData(je))
|
||||||
|
val generator = classNameGenerator()
|
||||||
|
|
||||||
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
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Write apk changes
|
// Write apk changes
|
||||||
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||||
|
@ -94,7 +161,6 @@ object HideAPK {
|
||||||
|
|
||||||
private fun launchApp(activity: Activity, pkg: String) {
|
private fun launchApp(activity: Activity, pkg: String) {
|
||||||
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||||
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
|
|
||||||
val self = activity.packageName
|
val self = activity.packageName
|
||||||
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
||||||
|
@ -103,17 +169,13 @@ object HideAPK {
|
||||||
activity.finish()
|
activity.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
||||||
val stub = File(activity.cacheDir, "stub.apk")
|
val stub = File(activity.cacheDir, "stub.apk")
|
||||||
try {
|
try {
|
||||||
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
|
activity.assets.open("stub.apk").writeTo(stub)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
stub.createNewFile()
|
return false
|
||||||
val cmd = "\$MAGISKBIN/magiskinit -x manager ${stub.path}"
|
|
||||||
if (!Shell.cmd(cmd).exec().isSuccess)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new random package name and signature
|
// Generate a new random package name and signature
|
||||||
|
@ -129,11 +191,12 @@ object HideAPK {
|
||||||
launchApp(activity, pkg)
|
launchApp(activity, pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
|
Config.suManager = pkg
|
||||||
|
val cmd = "adb_pm_install $repack $pkg"
|
||||||
if (Shell.cmd(cmd).exec().isSuccess) return true
|
if (Shell.cmd(cmd).exec().isSuccess) return true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session.install(activity, repack)
|
repack.inputStream().copyAndClose(session.openStream(activity))
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
|
@ -152,7 +215,7 @@ object HideAPK {
|
||||||
}
|
}
|
||||||
val onFailure = Runnable {
|
val onFailure = Runnable {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
val success = withContext(Dispatchers.IO) {
|
val success = withContext(Dispatchers.IO) {
|
||||||
patchAndHide(activity, label, onFailure)
|
patchAndHide(activity, label, onFailure)
|
||||||
|
@ -170,18 +233,19 @@ object HideAPK {
|
||||||
}
|
}
|
||||||
val onFailure = Runnable {
|
val onFailure = Runnable {
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
val apk = StubApk.current(activity)
|
val apk = StubApk.current(activity)
|
||||||
val session = APKInstall.startSession(activity, APPLICATION_ID, onFailure) {
|
val session = APKInstall.startSession(activity, APPLICATION_ID, onFailure) {
|
||||||
launchApp(activity, APPLICATION_ID)
|
launchApp(activity, APPLICATION_ID)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
|
Config.suManager = ""
|
||||||
|
val cmd = "adb_pm_install $apk $APPLICATION_ID"
|
||||||
if (Shell.cmd(cmd).await().isSuccess) return
|
if (Shell.cmd(cmd).await().isSuccess) return
|
||||||
val success = withContext(Dispatchers.IO) {
|
val success = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
session.install(activity, apk)
|
apk.inputStream().copyAndClose(session.openStream(activity))
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return@withContext false
|
return@withContext false
|
||||||
|
@ -191,4 +255,17 @@ object HideAPK {
|
||||||
}
|
}
|
||||||
if (!success) onFailure.run()
|
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,24 +1,32 @@
|
||||||
package com.topjohnwu.magisk.core.tasks
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Process
|
||||||
|
import android.system.ErrnoException
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
|
import android.system.OsConstants
|
||||||
|
import android.system.OsConstants.O_WRONLY
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
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.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
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
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.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import com.topjohnwu.superuser.internal.NOPList
|
import com.topjohnwu.superuser.internal.NOPList
|
||||||
|
@ -40,6 +48,7 @@ import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
abstract class MagiskInstallImpl protected constructor(
|
abstract class MagiskInstallImpl protected constructor(
|
||||||
protected val console: MutableList<String> = NOPList.getInstance(),
|
protected val console: MutableList<String> = NOPList.getInstance(),
|
||||||
|
@ -86,7 +95,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractFiles(): Boolean {
|
private suspend fun extractFiles(): Boolean {
|
||||||
console.add("- Device platform: ${Const.CPU_ABI}")
|
console.add("- Device platform: ${Const.CPU_ABI}")
|
||||||
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||||
|
|
||||||
|
@ -97,20 +106,23 @@ abstract class MagiskInstallImpl protected constructor(
|
||||||
try {
|
try {
|
||||||
// Extract binaries
|
// Extract binaries
|
||||||
if (isRunningAsStub) {
|
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 abi32 = Const.CPU_ABI_32
|
||||||
val is32lib = Const.CPU_ABI_32?.let {
|
if (Process.is64Bit() && abi32 != null) {
|
||||||
{ entry: ZipEntry -> entry.name == "lib/$it/libmagisk32.so" }
|
val magisk32 = File(installDir, "magisk32")
|
||||||
} ?: { false }
|
zf.getInputStream(ZipEntry("lib/$abi32/libmagisk.so")).writeTo(magisk32)
|
||||||
|
magisk32.setExecutable(true)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val info = context.applicationInfo
|
val info = context.applicationInfo
|
||||||
|
@ -118,20 +130,21 @@ abstract class MagiskInstallImpl protected constructor(
|
||||||
name.startsWith("lib") && name.endsWith(".so")
|
name.startsWith("lib") && name.endsWith(".so")
|
||||||
} ?: emptyArray()
|
} ?: 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) {
|
for (lib in libs) {
|
||||||
val name = lib.name.substring(3, lib.name.length - 3)
|
val name = lib.name.substring(3, lib.name.length - 3)
|
||||||
Os.symlink(lib.path, "$installDir/$name")
|
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
|
// 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)
|
val dest = File(installDir, script)
|
||||||
context.assets.open(script).writeTo(dest)
|
context.assets.open(script).writeTo(dest)
|
||||||
}
|
}
|
||||||
|
@ -164,101 +177,221 @@ abstract class MagiskInstallImpl protected constructor(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
|
private suspend fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyAll(it) }
|
||||||
src.copyTo(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newTarEntry(name: String, size: Long): TarEntry {
|
private fun newTarEntry(name: String, size: Long): TarEntry {
|
||||||
console.add("-- Writing: $name")
|
console.add("-- Writing: $name")
|
||||||
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
private class NoAvailableStream(s: InputStream) : FilterInputStream(s) {
|
||||||
private fun processTar(input: InputStream, output: OutputStream): OutputStream {
|
// Make sure available is never called on the actual stream and always return 0
|
||||||
console.add("- Processing tar file")
|
// 1. Workaround bug in LZ4FrameInputStream
|
||||||
val tarOut = TarOutputStream(output)
|
// 2. Reduce max buffer size to prevent OOM
|
||||||
TarInputStream(input).use { tarIn ->
|
override fun available() = 0
|
||||||
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 = installDir.getChildFile(name)
|
|
||||||
decompressedStream().cleanPump(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)
|
|
||||||
} else {
|
|
||||||
console.add("-- Copying: ${entry.name}")
|
|
||||||
tarOut.putNextEntry(entry)
|
|
||||||
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val boot = installDir.getChildFile("boot.img")
|
|
||||||
val recovery = installDir.getChildFile("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()
|
|
||||||
boot.newInputStream().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 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
|
val outStream: OutputStream
|
||||||
var outFile: MediaStoreUtils.UriFile? = null
|
val outFile: MediaStoreUtils.UriFile
|
||||||
|
|
||||||
// Process input file
|
// Process input file
|
||||||
try {
|
try {
|
||||||
uri.inputStream().buffered().use { src ->
|
PushbackInputStream(uri.inputStream(), 512).use { src ->
|
||||||
src.mark(500)
|
val head = ByteArray(512)
|
||||||
val magic = ByteArray(5)
|
if (src.read(head) != head.size) {
|
||||||
if (src.skip(257) != 257L || src.read(magic) != magic.size) {
|
|
||||||
console.add("! Invalid input file")
|
console.add("! Invalid input file")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
src.reset()
|
src.unread(head)
|
||||||
|
|
||||||
|
val magic = head.copyOf(4)
|
||||||
|
val tarMagic = Arrays.copyOfRange(head, 257, 262)
|
||||||
|
|
||||||
val alpha = "abcdefghijklmnopqrstuvwxyz"
|
val alpha = "abcdefghijklmnopqrstuvwxyz"
|
||||||
val alphaNum = "$alpha${alpha.uppercase(Locale.ROOT)}0123456789"
|
val alphaNum = "$alpha${alpha.uppercase(Locale.ROOT)}0123456789"
|
||||||
|
@ -270,29 +403,52 @@ abstract class MagiskInstallImpl protected constructor(
|
||||||
toString()
|
toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
|
||||||
// tar file
|
// tar file
|
||||||
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
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 {
|
} else {
|
||||||
// raw image
|
// raw image
|
||||||
srcBoot = installDir.getChildFile("boot.img")
|
|
||||||
console.add("- Copying image to cache")
|
|
||||||
src.cleanPump(srcBoot.newOutputStream())
|
|
||||||
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
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) {
|
} catch (e: IOException) {
|
||||||
|
if (e is NoBootException)
|
||||||
|
console.add("! No boot image found")
|
||||||
console.add("! Process error")
|
console.add("! Process error")
|
||||||
outFile?.delete()
|
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch file
|
// Patch file
|
||||||
if (!patchBoot()) {
|
if (!patchBoot()) {
|
||||||
outFile!!.delete()
|
outFile.delete()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,10 +456,16 @@ abstract class MagiskInstallImpl protected constructor(
|
||||||
try {
|
try {
|
||||||
val newBoot = installDir.getChildFile("new-boot.img")
|
val newBoot = installDir.getChildFile("new-boot.img")
|
||||||
if (outStream is TarOutputStream) {
|
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()))
|
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||||
}
|
}
|
||||||
newBoot.newInputStream().cleanPump(outStream)
|
newBoot.newInputStream().copyAndClose(outStream)
|
||||||
newBoot.delete()
|
newBoot.delete()
|
||||||
|
|
||||||
console.add("")
|
console.add("")
|
||||||
|
@ -313,7 +475,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||||
console.add("****************************")
|
console.add("****************************")
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Failed to output to $outFile")
|
console.add("! Failed to output to $outFile")
|
||||||
outFile!!.delete()
|
outFile.delete()
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -326,22 +488,6 @@ abstract class MagiskInstallImpl protected constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun patchBoot(): Boolean {
|
private fun patchBoot(): Boolean {
|
||||||
var isSigned = false
|
|
||||||
if (!srcBoot.isCharacter) {
|
|
||||||
try {
|
|
||||||
srcBoot.newInputStream().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 = installDir.getChildFile("new-boot.img")
|
val newBoot = installDir.getChildFile("new-boot.img")
|
||||||
if (!useRootDir) {
|
if (!useRootDir) {
|
||||||
// Create output files before hand
|
// Create output files before hand
|
||||||
|
@ -353,38 +499,20 @@ abstract class MagiskInstallImpl protected constructor(
|
||||||
"cd $installDir",
|
"cd $installDir",
|
||||||
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
|
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
|
||||||
"KEEPVERITY=${Config.keepVerity} " +
|
"KEEPVERITY=${Config.keepVerity} " +
|
||||||
"PATCHVBMETAFLAG=${Config.patchVbmeta} " +
|
"PATCHVBMETAFLAG=${Info.patchBootVbmeta} " +
|
||||||
"RECOVERYMODE=${Config.recovery} " +
|
"RECOVERYMODE=${Config.recovery} " +
|
||||||
|
"LEGACYSAR=${Info.legacySAR} " +
|
||||||
"sh boot_patch.sh $srcBoot")
|
"sh boot_patch.sh $srcBoot")
|
||||||
|
val isSuccess = cmds.sh().isSuccess
|
||||||
|
|
||||||
if (!cmds.sh().isSuccess)
|
shell.newJob().add("./magiskboot cleanup", "cd /").exec()
|
||||||
return false
|
|
||||||
|
|
||||||
val job = shell.newJob().add("./magiskboot cleanup", "cd /")
|
return isSuccess
|
||||||
|
|
||||||
if (isSigned) {
|
|
||||||
console.add("- Signing boot image with verity keys")
|
|
||||||
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
|
||||||
try {
|
|
||||||
val src = newBoot.newInputStream().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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
||||||
|
|
||||||
private fun postOTA(): Boolean {
|
private suspend fun postOTA(): Boolean {
|
||||||
try {
|
try {
|
||||||
val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
|
val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
|
||||||
context.assets.open("bootctl").writeTo(bootctl)
|
context.assets.open("bootctl").writeTo(bootctl)
|
||||||
|
@ -395,25 +523,27 @@ abstract class MagiskInstallImpl protected constructor(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
console.add("***************************************")
|
console.add("*************************************************************")
|
||||||
console.add(" Next reboot will boot to second slot!")
|
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
|
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 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 Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
|
||||||
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
|
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
|
||||||
|
|
||||||
protected fun patchFile(file: Uri) = extractFiles() && handleFile(file)
|
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 fun secondSlot() =
|
protected suspend fun secondSlot() =
|
||||||
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
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
|
protected fun uninstall() = "run_uninstaller $AppApkPath".sh().isSuccess
|
||||||
|
|
||||||
|
@ -501,7 +631,7 @@ abstract class MagiskInstaller(
|
||||||
override suspend fun exec(): Boolean {
|
override suspend fun exec(): Boolean {
|
||||||
val success = super.exec()
|
val success = super.exec()
|
||||||
callback()
|
callback()
|
||||||
Utils.toast(
|
context.toast(
|
||||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,7 +29,7 @@ class AXML(b: ByteArray) {
|
||||||
* Followed by an array of uint32_t with size = number of strings
|
* Followed by an array of uint32_t with size = number of strings
|
||||||
* Each entry points to an offset into the string data
|
* Each entry points to an offset into the string data
|
||||||
*/
|
*/
|
||||||
fun findAndPatch(vararg patterns: Pair<String, String>): Boolean {
|
fun patchStrings(patchFn: (Array<String>) -> Unit): Boolean {
|
||||||
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
|
||||||
|
|
||||||
fun findStringPool(): Int {
|
fun findStringPool(): Int {
|
||||||
|
@ -42,7 +42,6 @@ class AXML(b: ByteArray) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
var patch = false
|
|
||||||
val start = findStringPool()
|
val start = findStringPool()
|
||||||
if (start < 0)
|
if (start < 0)
|
||||||
return false
|
return false
|
||||||
|
@ -57,34 +56,26 @@ class AXML(b: ByteArray) {
|
||||||
val dataOff = start + intBuf.get()
|
val dataOff = start + intBuf.get()
|
||||||
intBuf.get()
|
intBuf.get()
|
||||||
|
|
||||||
val strings = ArrayList<String>(count)
|
val strList = ArrayList<String>(count)
|
||||||
// Read and patch all strings
|
// Collect all strings in the pool
|
||||||
loop@ for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
val off = dataOff + intBuf.get()
|
val off = dataOff + intBuf.get()
|
||||||
val len = buffer.getShort(off)
|
val len = buffer.getShort(off)
|
||||||
val str = String(bytes, off + 2, len * 2, UTF_16LE)
|
strList.add(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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!patch)
|
val strArr = strList.toTypedArray()
|
||||||
return false
|
patchFn(strArr)
|
||||||
|
|
||||||
// Write everything before string data, will patch values later
|
// Write everything before string data, will patch values later
|
||||||
val baos = RawByteStream()
|
val baos = RawByteStream()
|
||||||
baos.write(bytes, 0, dataOff)
|
baos.write(bytes, 0, dataOff)
|
||||||
|
|
||||||
// Write string data
|
// Write string data
|
||||||
val strList = IntArray(count)
|
val offList = IntArray(count)
|
||||||
for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
strList[i] = baos.size() - dataOff
|
offList[i] = baos.size() - dataOff
|
||||||
val str = strings[i]
|
val str = strArr[i]
|
||||||
baos.write(str.length.toShortBytes())
|
baos.write(str.length.toShortBytes())
|
||||||
baos.write(str.toByteArray(UTF_16LE))
|
baos.write(str.toByteArray(UTF_16LE))
|
||||||
// Null terminate
|
// Null terminate
|
||||||
|
@ -103,7 +94,7 @@ class AXML(b: ByteArray) {
|
||||||
// Patch index table
|
// Patch index table
|
||||||
newBuffer.position(start + STRING_INDICES_OFF)
|
newBuffer.position(start + STRING_INDICES_OFF)
|
||||||
val newIntBuf = newBuffer.asIntBuffer()
|
val newIntBuf = newBuffer.asIntBuffer()
|
||||||
strList.forEach { newIntBuf.put(it) }
|
offList.forEach { newIntBuf.put(it) }
|
||||||
|
|
||||||
// Write the rest of the chunks
|
// Write the rest of the chunks
|
||||||
val nextOff = start + size
|
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.core.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
|
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.AbstractExecutorService
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
|
@ -14,7 +14,9 @@ import java.security.KeyPairGenerator
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.cert.X509Certificate
|
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.GZIPInputStream
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import com.topjohnwu.magisk.core.createNewResources
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
var currentLocale: Locale = Locale.getDefault()
|
var currentLocale: Locale = Locale.getDefault()
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,6 @@ import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import kotlin.experimental.and
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
object MediaStoreUtils {
|
object MediaStoreUtils {
|
||||||
|
@ -87,7 +84,7 @@ object MediaStoreUtils {
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
|
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
|
// Fallback to file based I/O pre Android 11
|
||||||
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
||||||
parent.mkdirs()
|
parent.mkdirs()
|
||||||
|
@ -102,6 +99,8 @@ object MediaStoreUtils {
|
||||||
|
|
||||||
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
|
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() {
|
val Uri.displayName: String get() {
|
||||||
if (scheme == "file") {
|
if (scheme == "file") {
|
||||||
// Simple uri wrapper over file, directly get file name
|
// Simple uri wrapper over file, directly get file name
|
||||||
|
@ -118,24 +117,6 @@ object MediaStoreUtils {
|
||||||
return this.toString()
|
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 {
|
interface UriFile {
|
||||||
val uri: Uri
|
val uri: Uri
|
||||||
fun delete(): Boolean
|
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,
|
context: Context,
|
||||||
input: Unit
|
input: Unit
|
||||||
): SynchronousResult<Boolean>? {
|
): SynchronousResult<Boolean>? {
|
||||||
if (Build.VERSION.SDK_INT < 26)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||||
return SynchronousResult(true)
|
return SynchronousResult(true)
|
||||||
if (context.packageManager.canRequestPackageInstalls())
|
if (context.packageManager.canRequestPackageInstalls())
|
||||||
return SynchronousResult(true)
|
return SynchronousResult(true)
|
||||||
|
|
|
@ -111,15 +111,23 @@ class RootUtils(stub: Any?) : RootService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun await() {
|
fun await() {
|
||||||
// We cannot await on the main thread
|
if (!Info.isRooted)
|
||||||
if (Info.isRooted && !ShellUtils.onMainThread())
|
return
|
||||||
|
if (!ShellUtils.onMainThread()) {
|
||||||
acquireSharedInterruptibly(1)
|
acquireSharedInterruptibly(1)
|
||||||
|
} else if (state != 0) {
|
||||||
|
throw IllegalStateException("Cannot await on the main thread")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var bindTask: Shell.Task? = null
|
var bindTask: Shell.Task? = null
|
||||||
var fs = FileSystemManager.getLocal()
|
var fs: FileSystemManager = FileSystemManager.getLocal()
|
||||||
|
get() {
|
||||||
|
Connection.await()
|
||||||
|
return field
|
||||||
|
}
|
||||||
private set
|
private set
|
||||||
var obj: IRootUtils? = null
|
var obj: IRootUtils? = null
|
||||||
get() {
|
get() {
|
||||||
|
|
|
@ -7,12 +7,14 @@ import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||||
import com.topjohnwu.magisk.ktx.rawResource
|
import com.topjohnwu.magisk.core.ktx.rawResource
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.jar.JarFile
|
import java.util.jar.JarFile
|
||||||
|
|
||||||
|
@ -34,14 +36,16 @@ class ShellInit : Shell.Initializer() {
|
||||||
val bb = jar.getJarEntry("lib/${Const.CPU_ABI}/libbusybox.so")
|
val bb = jar.getJarEntry("lib/${Const.CPU_ABI}/libbusybox.so")
|
||||||
localBB = context.deviceProtectedContext.cachedFile("busybox")
|
localBB = context.deviceProtectedContext.cachedFile("busybox")
|
||||||
localBB.delete()
|
localBB.delete()
|
||||||
jar.getInputStream(bb).writeTo(localBB)
|
runBlocking {
|
||||||
|
jar.getInputStream(bb).writeTo(localBB, dispatcher = Dispatchers.Unconfined)
|
||||||
|
}
|
||||||
localBB.setExecutable(true)
|
localBB.setExecutable(true)
|
||||||
} else {
|
} else {
|
||||||
localBB = File(context.applicationInfo.nativeLibraryDir, "libbusybox.so")
|
localBB = File(context.applicationInfo.nativeLibraryDir, "libbusybox.so")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shell.isRoot) {
|
if (shell.isRoot) {
|
||||||
add("export MAGISKTMP=\$(magisk --path)/.magisk")
|
add("export MAGISKTMP=\$(magisk --path)")
|
||||||
// Test if we can properly execute stuff in /data
|
// Test if we can properly execute stuff in /data
|
||||||
Info.noDataExec = !shell.newJob().add("$localBB sh -c \"$localBB true\"").exec().isSuccess
|
Info.noDataExec = !shell.newJob().add("$localBB sh -c \"$localBB true\"").exec().isSuccess
|
||||||
}
|
}
|
||||||
|
@ -49,12 +53,12 @@ class ShellInit : Shell.Initializer() {
|
||||||
if (Info.noDataExec) {
|
if (Info.noDataExec) {
|
||||||
// Copy it out of /data to workaround Samsung bullshit
|
// Copy it out of /data to workaround Samsung bullshit
|
||||||
add(
|
add(
|
||||||
"if [ -x \$MAGISKTMP/busybox/busybox ]; then",
|
"if [ -x \$MAGISKTMP/.magisk/busybox/busybox ]; then",
|
||||||
" cp -af $localBB \$MAGISKTMP/busybox/busybox",
|
" cp -af $localBB \$MAGISKTMP/.magisk/busybox/busybox",
|
||||||
" exec \$MAGISKTMP/busybox/busybox sh",
|
" exec \$MAGISKTMP/.magisk/busybox/busybox sh",
|
||||||
"else",
|
"else",
|
||||||
" cp -af $localBB /dev/.busybox",
|
" cp -af $localBB /dev/busybox",
|
||||||
" exec /dev/.busybox sh",
|
" exec /dev/busybox sh",
|
||||||
"fi"
|
"fi"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -73,18 +77,17 @@ class ShellInit : Shell.Initializer() {
|
||||||
fun getVar(name: String) = fastCmd("echo \$$name")
|
fun getVar(name: String) = fastCmd("echo \$$name")
|
||||||
fun getBool(name: String) = getVar(name).toBoolean()
|
fun getBool(name: String) = getVar(name).toBoolean()
|
||||||
|
|
||||||
Const.MAGISKTMP = getVar("MAGISKTMP")
|
Info.isSAR = getBool("SYSTEM_AS_ROOT")
|
||||||
Info.isSAR = getBool("SYSTEM_ROOT")
|
|
||||||
Info.ramdisk = getBool("RAMDISKEXIST")
|
Info.ramdisk = getBool("RAMDISKEXIST")
|
||||||
Info.vbmeta = getBool("VBMETAEXIST")
|
|
||||||
Info.isAB = getBool("ISAB")
|
Info.isAB = getBool("ISAB")
|
||||||
Info.crypto = getVar("CRYPTOTYPE")
|
Info.crypto = getVar("CRYPTOTYPE")
|
||||||
|
Info.patchBootVbmeta = getBool("PATCHVBMETAFLAG")
|
||||||
|
Info.legacySAR = getBool("LEGACYSAR")
|
||||||
|
|
||||||
// Default presets
|
// Default presets
|
||||||
Config.recovery = getBool("RECOVERYMODE")
|
Config.recovery = getBool("RECOVERYMODE")
|
||||||
Config.keepVerity = getBool("KEEPVERITY")
|
Config.keepVerity = getBool("KEEPVERITY")
|
||||||
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
||||||
Config.patchVbmeta = getBool("PATCHVBMETAFLAG")
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
@ -7,14 +8,14 @@ import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@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 {
|
inputStream().buffered().use {
|
||||||
it.unzip(folder, path, junkPath)
|
it.unzip(folder, path, junkPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
suspend fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
||||||
try {
|
try {
|
||||||
val zin = ZipInputStream(this)
|
val zin = ZipInputStream(this)
|
||||||
var entry: ZipEntry
|
var entry: ZipEntry
|
||||||
|
@ -34,7 +35,7 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
||||||
if (!it.exists())
|
if (!it.exists())
|
||||||
it.mkdirs()
|
it.mkdirs()
|
||||||
}
|
}
|
||||||
dest.outputStream().use { out -> zin.copyTo(out) }
|
dest.outputStream().use { out -> zin.copyAll(out) }
|
||||||
}
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
throw IOException(e)
|
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() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,7 +8,12 @@ import android.text.Spanned
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
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.annotation.DrawableRes
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
|
@ -20,7 +25,11 @@ import androidx.databinding.BindingAdapter
|
||||||
import androidx.databinding.InverseBindingAdapter
|
import androidx.databinding.InverseBindingAdapter
|
||||||
import androidx.databinding.InverseBindingListener
|
import androidx.databinding.InverseBindingListener
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
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.button.MaterialButton
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
|
|
|
@ -1,37 +1,53 @@
|
||||||
package com.topjohnwu.magisk.databinding
|
package com.topjohnwu.magisk.databinding
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.databinding.ListChangeRegistry
|
import androidx.databinding.ListChangeRegistry
|
||||||
import androidx.databinding.ObservableList
|
import androidx.databinding.ObservableList
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
import java.util.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.AbstractList
|
||||||
|
|
||||||
/**
|
// Only expose the immutable List types
|
||||||
* @param callback The callback that controls the behavior of the DiffObservableList.
|
interface DiffList<T : DiffItem<*>> : List<T> {
|
||||||
* @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
|
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult
|
||||||
*/
|
|
||||||
open class DiffObservableList<T>(
|
|
||||||
private val callback: Callback<T>,
|
|
||||||
private val detectMoves: Boolean = true
|
|
||||||
) : AbstractList<T>(), ObservableList<T> {
|
|
||||||
|
|
||||||
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()
|
private val listeners = ListChangeRegistry()
|
||||||
protected val listCallback = ObservableListUpdateCallback()
|
|
||||||
|
|
||||||
override val size: Int get() = list.size
|
override val size: Int get() = list.size
|
||||||
|
|
||||||
/**
|
override fun get(index: Int) = list[index]
|
||||||
* Calculates the list of update operations that can convert this list into the given one.
|
|
||||||
*
|
override fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||||
* @param newItems The items that this list will be set to.
|
return doCalculateDiff(list, newItems)
|
||||||
* @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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
|
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
|
||||||
|
@ -40,47 +56,34 @@ open class DiffObservableList<T>(
|
||||||
|
|
||||||
override fun getNewListSize() = newItems.size
|
override fun getNewListSize() = newItems.size
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldItems[oldItemPosition]
|
val oldItem = oldItems[oldItemPosition]
|
||||||
val newItem = newItems[newItemPosition]
|
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 {
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = oldItems[oldItemPosition]
|
val oldItem = oldItems[oldItemPosition]
|
||||||
val newItem = newItems[newItemPosition]
|
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
|
@MainThread
|
||||||
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
override fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
||||||
list = newItems.toMutableList()
|
list = ArrayList(newItems)
|
||||||
diffResult.dispatchUpdatesTo(listCallback)
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@WorkerThread
|
||||||
* Sets this list to the given items. This is a convenience method for calling [ ][.calculateDiff] followed by [.update].
|
override suspend fun update(newItems: List<T>) {
|
||||||
*
|
val diffResult = calculateDiff(newItems)
|
||||||
*
|
withContext(Dispatchers.Main) {
|
||||||
* **Warning!** If the lists are large this operation may be too slow for the main thread. In
|
update(newItems, diffResult)
|
||||||
* 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
||||||
|
@ -91,113 +94,63 @@ open class DiffObservableList<T>(
|
||||||
listeners.remove(listener)
|
listeners.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun get(index: Int) = list[index]
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
|
listeners.notifyChanged(this, position, count)
|
||||||
override fun add(index: Int, element: T) {
|
|
||||||
list.add(index, element)
|
|
||||||
notifyAdd(index, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addAll(elements: Collection<T>) = addAll(size, elements)
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||||
|
listeners.notifyMoved(this, fromPosition, toPosition, 1)
|
||||||
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 clear() {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
val oldSize = size
|
modCount += 1
|
||||||
list.clear()
|
listeners.notifyInserted(this, position, count)
|
||||||
if (oldSize != 0) {
|
|
||||||
notifyRemove(0, oldSize)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun remove(element: T): Boolean {
|
override fun onRemoved(position: Int, count: Int) {
|
||||||
val index = indexOf(element)
|
modCount += 1
|
||||||
return if (index >= 0) {
|
listeners.notifyRemoved(this, position, count)
|
||||||
removeAt(index)
|
}
|
||||||
true
|
}
|
||||||
} else {
|
|
||||||
false
|
private class FilterableDiffObservableList<T : DiffItem<*>>(
|
||||||
}
|
private val scope: CoroutineScope
|
||||||
}
|
) : DiffObservableList<T>(), FilterList<T> {
|
||||||
|
|
||||||
override fun removeAt(index: Int): T {
|
private var sublist: List<T> = emptyList()
|
||||||
val element = list.removeAt(index)
|
private var job: Job? = null
|
||||||
notifyRemove(index, 1)
|
private var lastFilter: ((T) -> Boolean)? = null
|
||||||
return element
|
|
||||||
}
|
// ---
|
||||||
|
|
||||||
override fun set(index: Int, element: T): T {
|
override fun filter(filter: (T) -> Boolean) {
|
||||||
val old = list.set(index, element)
|
lastFilter = filter
|
||||||
listeners.notifyChanged(this, index, 1)
|
job?.cancel()
|
||||||
return old
|
job = scope.launch(Dispatchers.Default) {
|
||||||
}
|
val oldList = sublist
|
||||||
|
val newList = list.filter(filter)
|
||||||
private fun notifyAdd(start: Int, count: Int) {
|
val diff = doCalculateDiff(oldList, newList)
|
||||||
listeners.notifyInserted(this, start, count)
|
withContext(Dispatchers.Main) {
|
||||||
}
|
sublist = newList
|
||||||
|
diff.dispatchUpdatesTo(this@FilterableDiffObservableList)
|
||||||
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.
|
|
||||||
*/
|
override fun get(index: Int): T {
|
||||||
interface Callback<T> {
|
return sublist[index]
|
||||||
/**
|
}
|
||||||
* Called by the DiffUtil to decide whether two object represent the same Item.
|
|
||||||
*
|
override val size: Int
|
||||||
*
|
get() = sublist.size
|
||||||
* For example, if your items have unique ids, this method should check their id equality.
|
|
||||||
*
|
@MainThread
|
||||||
* @param oldItem The old item.
|
override fun set(newItems: List<T>) {
|
||||||
* @param newItem The new item.
|
onRemoved(0, sublist.size)
|
||||||
* @return True if the two items represent the same object or false if they are different.
|
list = newItems
|
||||||
*/
|
sublist = emptyList()
|
||||||
fun areItemsTheSame(oldItem: T, newItem: T): Boolean
|
lastFilter?.let { filter(it) }
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,7 +0,0 @@
|
||||||
package com.topjohnwu.magisk.databinding
|
|
||||||
|
|
||||||
fun <T : AnyDiffRvItem> diffListOf() =
|
|
||||||
DiffObservableList(DiffRvItem.callback<T>())
|
|
||||||
|
|
||||||
fun <T : AnyDiffRvItem> filterableListOf() =
|
|
||||||
FilterableDiffObservableList(DiffRvItem.callback<T>())
|
|
|
@ -3,7 +3,7 @@ package com.topjohnwu.magisk.databinding
|
||||||
import androidx.databinding.ListChangeRegistry
|
import androidx.databinding.ListChangeRegistry
|
||||||
import androidx.databinding.ObservableList
|
import androidx.databinding.ObservableList
|
||||||
import androidx.databinding.ObservableList.OnListChangedCallback
|
import androidx.databinding.ObservableList.OnListChangedCallback
|
||||||
import java.util.*
|
import java.util.AbstractList
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
||||||
|
@ -46,11 +46,11 @@ class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertList(list: ObservableList<out T>): MergeObservableList<T> {
|
fun insertList(list: List<T>): MergeObservableList<T> {
|
||||||
val idx = size
|
val idx = size
|
||||||
lists.add(list)
|
lists.add(list)
|
||||||
++modCount
|
++modCount
|
||||||
(list as ObservableList<T>).addOnListChangedCallback(callback)
|
(list as? ObservableList<T>)?.addOnListChangedCallback(callback)
|
||||||
if (list.isNotEmpty())
|
if (list.isNotEmpty())
|
||||||
listeners.notifyInserted(this, idx, list.size)
|
listeners.notifyInserted(this, idx, list.size)
|
||||||
return this
|
return this
|
||||||
|
@ -72,11 +72,11 @@ class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeList(listToRemove: ObservableList<out T>): Boolean {
|
fun removeList(listToRemove: List<T>): Boolean {
|
||||||
var idx = 0
|
var idx = 0
|
||||||
for ((i, list) in lists.withIndex()) {
|
for ((i, list) in lists.withIndex()) {
|
||||||
if (listToRemove === list) {
|
if (listToRemove === list) {
|
||||||
(list as ObservableList<T>).removeOnListChangedCallback(callback)
|
(list as? ObservableList<T>)?.removeOnListChangedCallback(callback)
|
||||||
lists.removeAt(i)
|
lists.removeAt(i)
|
||||||
++modCount
|
++modCount
|
||||||
listeners.notifyRemoved(this, idx, list.size)
|
listeners.notifyRemoved(this, idx, list.size)
|
||||||
|
@ -90,8 +90,8 @@ class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
||||||
override fun clear() {
|
override fun clear() {
|
||||||
val sz = size
|
val sz = size
|
||||||
for (list in lists) {
|
for (list in lists) {
|
||||||
if (list is ObservableList<*>) {
|
if (list is ObservableList) {
|
||||||
(list as ObservableList<T>).removeOnListChangedCallback(callback)
|
list.removeOnListChangedCallback(callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
++modCount
|
++modCount
|
||||||
|
|
|
@ -8,60 +8,28 @@ abstract class RvItem {
|
||||||
abstract val layoutRes: Int
|
abstract val layoutRes: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RvContainer<E> {
|
|
||||||
val item: E
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ViewAwareRvItem {
|
|
||||||
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComparableRv<T> : Comparable<T> {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun comparableEqual(o: Any?) =
|
|
||||||
o != null && o::class == this::class && compareTo(o as T) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class DiffRvItem<T> : RvItem() {
|
|
||||||
|
|
||||||
// Defer to contentSameAs by default
|
|
||||||
open fun itemSameAs(other: T) = true
|
|
||||||
|
|
||||||
open fun contentSameAs(other: T) =
|
|
||||||
when (this) {
|
|
||||||
is RvContainer<*> -> item == (other as RvContainer<*>).item
|
|
||||||
is ComparableRv<*> -> comparableEqual(other)
|
|
||||||
else -> this == other
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val callback = object : DiffObservableList.Callback<DiffRvItem<Any>> {
|
|
||||||
override fun areItemsTheSame(
|
|
||||||
oldItem: DiffRvItem<Any>,
|
|
||||||
newItem: DiffRvItem<Any>
|
|
||||||
): Boolean {
|
|
||||||
return oldItem::class == newItem::class && oldItem.itemSameAs(newItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: DiffRvItem<Any>,
|
|
||||||
newItem: DiffRvItem<Any>
|
|
||||||
): Boolean {
|
|
||||||
return oldItem.contentSameAs(newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : AnyDiffRvItem> callback() = callback as DiffObservableList.Callback<T>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias AnyDiffRvItem = DiffRvItem<*>
|
|
||||||
|
|
||||||
abstract class ObservableDiffRvItem<T> : DiffRvItem<T>(), ObservableHost {
|
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
override var callbacks: PropertyChangeRegistry? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ItemWrapper<E> {
|
||||||
|
val item: E
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ViewAwareItem {
|
||||||
|
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiffItem<T : Any> {
|
||||||
|
|
||||||
|
fun itemSameAs(other: T): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
return when (this) {
|
||||||
|
is ItemWrapper<*> -> item == (other as ItemWrapper<*>).item
|
||||||
|
is Comparable<*> -> compareValues(this, other as Comparable<*>) == 0
|
||||||
|
else -> this == other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contentSameAs(other: T) = true
|
||||||
|
}
|
||||||
|
|
|
@ -15,8 +15,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
|
|
||||||
class RvItemAdapter<T: RvItem>(
|
class RvItemAdapter<T: RvItem>(
|
||||||
private val items: List<T>,
|
val items: List<T>,
|
||||||
private val extraBindings: SparseArray<*>?
|
val extraBindings: SparseArray<*>?
|
||||||
) : RecyclerView.Adapter<RvItemAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<RvItemAdapter.ViewHolder>() {
|
||||||
|
|
||||||
private var lifecycleOwner: LifecycleOwner? = null
|
private var lifecycleOwner: LifecycleOwner? = null
|
||||||
|
@ -53,7 +53,7 @@ class RvItemAdapter<T: RvItem>(
|
||||||
holder.binding.lifecycleOwner = lifecycleOwner
|
holder.binding.lifecycleOwner = lifecycleOwner
|
||||||
holder.binding.executePendingBindings()
|
holder.binding.executePendingBindings()
|
||||||
recyclerView?.let {
|
recyclerView?.let {
|
||||||
if (item is ViewAwareRvItem)
|
if (item is ViewAwareItem)
|
||||||
item.onBind(holder.binding, it)
|
item.onBind(holder.binding, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,9 @@ inline fun bindExtra(body: (SparseArray<Any?>) -> Unit) = SparseArray<Any?>().al
|
||||||
@BindingAdapter("items", "extraBindings", requireAll = false)
|
@BindingAdapter("items", "extraBindings", requireAll = false)
|
||||||
fun <T: RvItem> RecyclerView.setAdapter(items: List<T>?, extraBindings: SparseArray<*>?) {
|
fun <T: RvItem> RecyclerView.setAdapter(items: List<T>?, extraBindings: SparseArray<*>?) {
|
||||||
if (items != null) {
|
if (items != null) {
|
||||||
adapter = RvItemAdapter(items, extraBindings)
|
val rva = (adapter as? RvItemAdapter<*>)
|
||||||
|
if (rva == null || rva.items !== items || rva.extraBindings !== extraBindings) {
|
||||||
|
adapter = RvItemAdapter(items, extraBindings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.UIActivity
|
import com.topjohnwu.magisk.arch.UIActivity
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
class DarkThemeDialog : DialogEvent() {
|
class DarkThemeDialog : DialogBuilder {
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
val activity = dialog.ownerActivity!!
|
val activity = dialog.ownerActivity!!
|
|
@ -1,4 +1,4 @@
|
||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
@ -6,11 +6,12 @@ import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class EnvFixDialog(private val vm: HomeViewModel) : DialogEvent() {
|
class EnvFixDialog(private val vm: HomeViewModel, private val code: Int) : DialogBuilder {
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
dialog.apply {
|
dialog.apply {
|
||||||
|
@ -38,8 +39,10 @@ class EnvFixDialog(private val vm: HomeViewModel) : DialogEvent() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Info.env.versionCode != BuildConfig.VERSION_CODE ||
|
if (code == 2 || // No rules block, module policy not loaded
|
||||||
|
Info.env.versionCode != BuildConfig.VERSION_CODE ||
|
||||||
Info.env.versionString != BuildConfig.VERSION_NAME) {
|
Info.env.versionString != BuildConfig.VERSION_NAME) {
|
||||||
|
dialog.setMessage(R.string.env_full_fix_msg)
|
||||||
dialog.setButton(MagiskDialog.ButtonType.POSITIVE) {
|
dialog.setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
text = android.R.string.ok
|
text = android.R.string.ok
|
||||||
onClick {
|
onClick {
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.topjohnwu.magisk.MainDirections
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
|
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||||
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
|
class LocalModuleInstallDialog(
|
||||||
|
private val viewModel: ModuleViewModel,
|
||||||
|
private val uri: Uri,
|
||||||
|
private val displayName: String
|
||||||
|
) : DialogBuilder {
|
||||||
|
override fun build(dialog: MagiskDialog) {
|
||||||
|
dialog.apply {
|
||||||
|
setTitle(R.string.confirm_install_title)
|
||||||
|
setMessage(context.getString(R.string.confirm_install, displayName))
|
||||||
|
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
|
text = android.R.string.ok
|
||||||
|
onClick {
|
||||||
|
viewModel.apply {
|
||||||
|
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, uri).navigate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
|
text = android.R.string.cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() {
|
||||||
setCancelable(true)
|
setCancelable(true)
|
||||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
text = R.string.install
|
text = R.string.install
|
||||||
onClick { DownloadService.start(context, Subject.App()) }
|
onClick { DownloadEngine.startWithActivity(activity, Subject.App()) }
|
||||||
}
|
}
|
||||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
text = android.R.string.cancel
|
text = android.R.string.cancel
|
|
@ -1,12 +1,12 @@
|
||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -14,7 +14,7 @@ import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
abstract class MarkDownDialog : DialogEvent() {
|
abstract class MarkDownDialog : DialogBuilder {
|
||||||
|
|
||||||
abstract suspend fun getMarkdownText(): String
|
abstract suspend fun getMarkdownText(): String
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ abstract class MarkDownDialog : DialogEvent() {
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
|
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
|
||||||
setView(view)
|
setView(view)
|
||||||
val tv = view.findViewById<TextView>(R.id.md_txt)
|
val tv = view.findViewById<TextView>(R.id.md_txt)
|
||||||
(ownerActivity as BaseActivity).lifecycleScope.launch {
|
activity.lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val text = withContext(Dispatchers.IO) { getMarkdownText() }
|
val text = withContext(Dispatchers.IO) { getMarkdownText() }
|
||||||
ServiceLocator.markwon.setMarkdown(tv, text)
|
ServiceLocator.markwon.setMarkdown(tv, text)
|
|
@ -1,14 +1,13 @@
|
||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.download.Action
|
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
class ModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
class OnlineModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
||||||
|
|
||||||
private val svc get() = ServiceLocator.networkService
|
private val svc get() = ServiceLocator.networkService
|
||||||
|
|
||||||
|
@ -22,9 +21,7 @@ class ModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
||||||
dialog.apply {
|
dialog.apply {
|
||||||
|
|
||||||
fun download(install: Boolean) {
|
fun download(install: Boolean) {
|
||||||
val action = if (install) Action.Flash else Action.Download
|
DownloadEngine.startWithActivity(activity, Subject.Module(item, install))
|
||||||
val subject = Subject.Module(item, action)
|
|
||||||
DownloadService.start(context, subject)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val title = context.getString(R.string.repo_install_title,
|
val title = context.getString(R.string.repo_install_title,
|
|
@ -1,9 +1,10 @@
|
||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
class SecondSlotWarningDialog : DialogEvent() {
|
class SecondSlotWarningDialog : DialogBuilder {
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
dialog.apply {
|
dialog.apply {
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
|
class SuperuserRevokeDialog(
|
||||||
|
private val appName: String,
|
||||||
|
private val onSuccess: () -> Unit
|
||||||
|
) : DialogBuilder {
|
||||||
|
|
||||||
|
override fun build(dialog: MagiskDialog) {
|
||||||
|
dialog.apply {
|
||||||
|
setTitle(R.string.su_revoke_title)
|
||||||
|
setMessage(R.string.su_revoke_msg, appName)
|
||||||
|
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
|
text = android.R.string.ok
|
||||||
|
onClick { onSuccess() }
|
||||||
|
}
|
||||||
|
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
|
text = android.R.string.cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,17 @@
|
||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.dialog
|
||||||
|
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.NavigationActivity
|
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.events.DialogBuilder
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
class UninstallDialog : DialogEvent() {
|
class UninstallDialog : DialogBuilder {
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
dialog.apply {
|
dialog.apply {
|
||||||
|
@ -37,9 +38,9 @@ class UninstallDialog : DialogEvent() {
|
||||||
Shell.cmd("restore_imgs").submit { result ->
|
Shell.cmd("restore_imgs").submit { result ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
Utils.toast(R.string.restore_done, Toast.LENGTH_SHORT)
|
context.toast(R.string.restore_done, Toast.LENGTH_SHORT)
|
||||||
} else {
|
} else {
|
||||||
Utils.toast(R.string.restore_fail, Toast.LENGTH_LONG)
|
context.toast(R.string.restore_fail, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,10 +5,15 @@ import android.view.View
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.topjohnwu.magisk.arch.*
|
import com.topjohnwu.magisk.arch.ActivityExecutor
|
||||||
|
import com.topjohnwu.magisk.arch.ContextExecutor
|
||||||
|
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||||
|
import com.topjohnwu.magisk.arch.UIActivity
|
||||||
|
import com.topjohnwu.magisk.arch.ViewEvent
|
||||||
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
||||||
import com.topjohnwu.magisk.utils.TextHolder
|
import com.topjohnwu.magisk.utils.TextHolder
|
||||||
import com.topjohnwu.magisk.utils.asText
|
import com.topjohnwu.magisk.utils.asText
|
||||||
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
|
|
||||||
class PermissionEvent(
|
class PermissionEvent(
|
||||||
|
@ -46,6 +51,16 @@ class RecreateEvent : ViewEvent(), ActivityExecutor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AuthEvent(
|
||||||
|
private val callback: () -> Unit
|
||||||
|
) : ViewEvent(), ActivityExecutor {
|
||||||
|
|
||||||
|
override fun invoke(activity: UIActivity<*>) {
|
||||||
|
activity.authenticateCallback = { if (it) callback() }
|
||||||
|
activity.requestAuthenticate.launch(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class GetContentEvent(
|
class GetContentEvent(
|
||||||
private val type: String,
|
private val type: String,
|
||||||
private val callback: ContentResultCallback
|
private val callback: ContentResultCallback
|
||||||
|
@ -95,3 +110,15 @@ class SnackbarEvent(
|
||||||
activity.showSnackbar(msg.getText(activity.resources), length, builder)
|
activity.showSnackbar(msg.getText(activity.resources), length, builder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DialogEvent(
|
||||||
|
private val builder: DialogBuilder
|
||||||
|
) : ViewEvent(), ActivityExecutor {
|
||||||
|
override fun invoke(activity: UIActivity<*>) {
|
||||||
|
MagiskDialog(activity).apply(builder::build).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DialogBuilder {
|
||||||
|
fun build(dialog: MagiskDialog)
|
||||||
|
}
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
package com.topjohnwu.magisk.events.dialog
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.arch.ActivityExecutor
|
|
||||||
import com.topjohnwu.magisk.arch.UIActivity
|
|
||||||
import com.topjohnwu.magisk.arch.ViewEvent
|
|
||||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
|
||||||
|
|
||||||
class BiometricEvent(
|
|
||||||
builder: Builder.() -> Unit
|
|
||||||
) : ViewEvent(), ActivityExecutor {
|
|
||||||
|
|
||||||
private var listenerOnFailure: GenericDialogListener = {}
|
|
||||||
private var listenerOnSuccess: GenericDialogListener = {}
|
|
||||||
|
|
||||||
init {
|
|
||||||
builder(Builder())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun invoke(activity: UIActivity<*>) {
|
|
||||||
BiometricHelper.authenticate(
|
|
||||||
activity,
|
|
||||||
onError = listenerOnFailure,
|
|
||||||
onSuccess = listenerOnSuccess
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Builder internal constructor() {
|
|
||||||
|
|
||||||
fun onFailure(listener: GenericDialogListener) {
|
|
||||||
listenerOnFailure = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSuccess(listener: GenericDialogListener) {
|
|
||||||
listenerOnSuccess = listener
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package com.topjohnwu.magisk.events.dialog
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.arch.ActivityExecutor
|
|
||||||
import com.topjohnwu.magisk.arch.UIActivity
|
|
||||||
import com.topjohnwu.magisk.arch.ViewEvent
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
|
||||||
|
|
||||||
abstract class DialogEvent : ViewEvent(), ActivityExecutor {
|
|
||||||
|
|
||||||
override fun invoke(activity: UIActivity<*>) {
|
|
||||||
MagiskDialog(activity)
|
|
||||||
.apply { setOwnerActivity(activity) }
|
|
||||||
.apply(this::build).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun build(dialog: MagiskDialog)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
typealias GenericDialogListener = () -> Unit
|
|
|
@ -1,35 +0,0 @@
|
||||||
package com.topjohnwu.magisk.events.dialog
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
|
||||||
|
|
||||||
class SuperuserRevokeDialog(
|
|
||||||
builder: Builder.() -> Unit
|
|
||||||
) : DialogEvent() {
|
|
||||||
|
|
||||||
private val callbacks = Builder().apply(builder)
|
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
|
||||||
dialog.apply {
|
|
||||||
setTitle(R.string.su_revoke_title)
|
|
||||||
setMessage(R.string.su_revoke_msg, callbacks.appName)
|
|
||||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
|
||||||
text = android.R.string.ok
|
|
||||||
onClick { callbacks.listenerOnSuccess() }
|
|
||||||
}
|
|
||||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
|
||||||
text = android.R.string.cancel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Builder internal constructor() {
|
|
||||||
var appName: String = ""
|
|
||||||
|
|
||||||
internal var listenerOnSuccess: GenericDialogListener = {}
|
|
||||||
|
|
||||||
fun onSuccess(listener: GenericDialogListener) {
|
|
||||||
listenerOnSuccess = listener
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,267 +0,0 @@
|
||||||
package com.topjohnwu.magisk.ktx
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.ApplicationInfo
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
||||||
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.net.Uri
|
|
||||||
import android.os.Build.VERSION.SDK_INT
|
|
||||||
import android.os.Process
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
|
||||||
import androidx.transition.AutoTransition
|
|
||||||
import androidx.transition.TransitionManager
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.Array
|
|
||||||
import kotlin.String
|
|
||||||
import java.lang.reflect.Array as JArray
|
|
||||||
|
|
||||||
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 >= 26 && 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 >= 24) {
|
|
||||||
createDeviceProtectedStorageContext()
|
|
||||||
} else { this }
|
|
||||||
|
|
||||||
fun Intent.startActivityWithRoot() {
|
|
||||||
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
|
||||||
val cmd = toCommand(args).joinToString(" ")
|
|
||||||
Shell.cmd(cmd).submit()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<String> {
|
|
||||||
action?.also {
|
|
||||||
args.add("-a")
|
|
||||||
args.add(it)
|
|
||||||
}
|
|
||||||
component?.also {
|
|
||||||
args.add("-n")
|
|
||||||
args.add(it.flattenToString())
|
|
||||||
}
|
|
||||||
data?.also {
|
|
||||||
args.add("-d")
|
|
||||||
args.add(it.toString())
|
|
||||||
}
|
|
||||||
categories?.also {
|
|
||||||
for (cat in it) {
|
|
||||||
args.add("-c")
|
|
||||||
args.add(cat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type?.also {
|
|
||||||
args.add("-t")
|
|
||||||
args.add(it)
|
|
||||||
}
|
|
||||||
extras?.also {
|
|
||||||
loop@ for (key in it.keySet()) {
|
|
||||||
val v = it[key] ?: continue
|
|
||||||
var value: Any = v
|
|
||||||
val arg: String
|
|
||||||
when {
|
|
||||||
v is String -> arg = "--es"
|
|
||||||
v is Boolean -> arg = "--ez"
|
|
||||||
v is Int -> arg = "--ei"
|
|
||||||
v is Long -> arg = "--el"
|
|
||||||
v is Float -> arg = "--ef"
|
|
||||||
v is Uri -> arg = "--eu"
|
|
||||||
v is ComponentName -> {
|
|
||||||
arg = "--ecn"
|
|
||||||
value = v.flattenToString()
|
|
||||||
}
|
|
||||||
v is List<*> -> {
|
|
||||||
if (v.isEmpty())
|
|
||||||
continue@loop
|
|
||||||
|
|
||||||
arg = if (v[0] is Int)
|
|
||||||
"--eial"
|
|
||||||
else if (v[0] is Long)
|
|
||||||
"--elal"
|
|
||||||
else if (v[0] is Float)
|
|
||||||
"--efal"
|
|
||||||
else if (v[0] is String)
|
|
||||||
"--esal"
|
|
||||||
else
|
|
||||||
continue@loop /* Unsupported */
|
|
||||||
|
|
||||||
val sb = StringBuilder()
|
|
||||||
for (o in v) {
|
|
||||||
sb.append(o.toString().replace(",", "\\,"))
|
|
||||||
sb.append(',')
|
|
||||||
}
|
|
||||||
// Remove trailing comma
|
|
||||||
sb.deleteCharAt(sb.length - 1)
|
|
||||||
value = sb
|
|
||||||
}
|
|
||||||
v.javaClass.isArray -> {
|
|
||||||
arg = if (v is IntArray)
|
|
||||||
"--eia"
|
|
||||||
else if (v is LongArray)
|
|
||||||
"--ela"
|
|
||||||
else if (v is FloatArray)
|
|
||||||
"--efa"
|
|
||||||
else if (v is Array<*> && v.isArrayOf<String>())
|
|
||||||
"--esa"
|
|
||||||
else
|
|
||||||
continue@loop /* Unsupported */
|
|
||||||
|
|
||||||
val sb = StringBuilder()
|
|
||||||
val len = JArray.getLength(v)
|
|
||||||
for (i in 0 until len) {
|
|
||||||
sb.append(JArray.get(v, i)!!.toString().replace(",", "\\,"))
|
|
||||||
sb.append(',')
|
|
||||||
}
|
|
||||||
// Remove trailing comma
|
|
||||||
sb.deleteCharAt(sb.length - 1)
|
|
||||||
value = sb
|
|
||||||
}
|
|
||||||
else -> continue@loop
|
|
||||||
} /* Unsupported */
|
|
||||||
|
|
||||||
args.add(arg)
|
|
||||||
args.add(key)
|
|
||||||
args.add(value.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
args.add("-f")
|
|
||||||
args.add(flags.toString())
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (true) {
|
|
||||||
if (context is ContextWrapper)
|
|
||||||
context = context.baseContext
|
|
||||||
else
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.hasPermissions(vararg permissions: String) = permissions.all {
|
|
||||||
ContextCompat.checkSelfPermission(this, it) == PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.hideKeyboard() {
|
|
||||||
val view = currentFocus ?: return
|
|
||||||
getSystemService<InputMethodManager>()
|
|
||||||
?.hideSoftInputFromWindow(view.windowToken, 0)
|
|
||||||
view.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ViewGroup.startAnimations() {
|
|
||||||
val transition = AutoTransition()
|
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
|
||||||
.setDuration(400)
|
|
||||||
.excludeTarget(R.id.main_toolbar, true)
|
|
||||||
TransitionManager.beginDelayedTransition(
|
|
||||||
this,
|
|
||||||
transition
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
|
@ -680,7 +680,7 @@ public abstract class ApkSignerV2 {
|
||||||
return "SHA-512";
|
return "SHA-512";
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Unknown content digest algorthm: " + digestAlgorithm);
|
"Unknown content digest algorithm: " + digestAlgorithm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -692,7 +692,7 @@ public abstract class ApkSignerV2 {
|
||||||
return 512 / 8;
|
return 512 / 8;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Unknown content digest algorthm: " + digestAlgorithm);
|
"Unknown content digest algorithm: " + digestAlgorithm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
package com.topjohnwu.magisk.signing;
|
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1InputStream;
|
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
|
||||||
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
|
|
||||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
|
||||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
|
||||||
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.security.Key;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.security.spec.ECPrivateKeySpec;
|
|
||||||
import java.security.spec.ECPublicKeySpec;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class CryptoUtils {
|
|
||||||
|
|
||||||
static final Map<String, String> ID_TO_ALG;
|
|
||||||
static final Map<String, String> ALG_TO_ID;
|
|
||||||
|
|
||||||
static {
|
|
||||||
ID_TO_ALG = new HashMap<>();
|
|
||||||
ALG_TO_ID = new HashMap<>();
|
|
||||||
ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA256.getId(), "SHA256withECDSA");
|
|
||||||
ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA384.getId(), "SHA384withECDSA");
|
|
||||||
ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA512.getId(), "SHA512withECDSA");
|
|
||||||
ID_TO_ALG.put(PKCSObjectIdentifiers.sha1WithRSAEncryption.getId(), "SHA1withRSA");
|
|
||||||
ID_TO_ALG.put(PKCSObjectIdentifiers.sha256WithRSAEncryption.getId(), "SHA256withRSA");
|
|
||||||
ID_TO_ALG.put(PKCSObjectIdentifiers.sha512WithRSAEncryption.getId(), "SHA512withRSA");
|
|
||||||
ALG_TO_ID.put("SHA256withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA256.getId());
|
|
||||||
ALG_TO_ID.put("SHA384withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA384.getId());
|
|
||||||
ALG_TO_ID.put("SHA512withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA512.getId());
|
|
||||||
ALG_TO_ID.put("SHA1withRSA", PKCSObjectIdentifiers.sha1WithRSAEncryption.getId());
|
|
||||||
ALG_TO_ID.put("SHA256withRSA", PKCSObjectIdentifiers.sha256WithRSAEncryption.getId());
|
|
||||||
ALG_TO_ID.put("SHA512withRSA", PKCSObjectIdentifiers.sha512WithRSAEncryption.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getSignatureAlgorithm(Key key) throws Exception {
|
|
||||||
if ("EC".equals(key.getAlgorithm())) {
|
|
||||||
int curveSize;
|
|
||||||
KeyFactory factory = KeyFactory.getInstance("EC");
|
|
||||||
if (key instanceof PublicKey) {
|
|
||||||
ECPublicKeySpec spec = factory.getKeySpec(key, ECPublicKeySpec.class);
|
|
||||||
curveSize = spec.getParams().getCurve().getField().getFieldSize();
|
|
||||||
} else if (key instanceof PrivateKey) {
|
|
||||||
ECPrivateKeySpec spec = factory.getKeySpec(key, ECPrivateKeySpec.class);
|
|
||||||
curveSize = spec.getParams().getCurve().getField().getFieldSize();
|
|
||||||
} else {
|
|
||||||
throw new InvalidKeySpecException();
|
|
||||||
}
|
|
||||||
if (curveSize <= 256) {
|
|
||||||
return "SHA256withECDSA";
|
|
||||||
} else if (curveSize <= 384) {
|
|
||||||
return "SHA384withECDSA";
|
|
||||||
} else {
|
|
||||||
return "SHA512withECDSA";
|
|
||||||
}
|
|
||||||
} else if ("RSA".equals(key.getAlgorithm())) {
|
|
||||||
return "SHA256withRSA";
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static AlgorithmIdentifier getSignatureAlgorithmIdentifier(Key key) throws Exception {
|
|
||||||
String id = ALG_TO_ID.get(getSignatureAlgorithm(key));
|
|
||||||
if (id == null) {
|
|
||||||
throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm());
|
|
||||||
}
|
|
||||||
return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static X509Certificate readCertificate(InputStream input)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
try {
|
|
||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
|
||||||
return (X509Certificate) cf.generateCertificate(input);
|
|
||||||
} finally {
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Read a PKCS#8 format private key. */
|
|
||||||
public static PrivateKey readPrivateKey(InputStream input)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
try {
|
|
||||||
ByteArrayStream buf = new ByteArrayStream();
|
|
||||||
buf.readFrom(input);
|
|
||||||
byte[] bytes = buf.toByteArray();
|
|
||||||
/* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
|
|
||||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
|
|
||||||
/*
|
|
||||||
* Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
|
|
||||||
* OID and use that to construct a KeyFactory.
|
|
||||||
*/
|
|
||||||
ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()));
|
|
||||||
PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
|
|
||||||
String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
|
|
||||||
return KeyFactory.getInstance(algOid).generatePrivate(spec);
|
|
||||||
} finally {
|
|
||||||
input.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,382 +0,0 @@
|
||||||
package com.topjohnwu.magisk.signing;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.bouncycastle.asn1.ASN1Encodable;
|
|
||||||
import org.bouncycastle.asn1.ASN1EncodableVector;
|
|
||||||
import org.bouncycastle.asn1.ASN1InputStream;
|
|
||||||
import org.bouncycastle.asn1.ASN1Integer;
|
|
||||||
import org.bouncycastle.asn1.ASN1Object;
|
|
||||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
|
||||||
import org.bouncycastle.asn1.ASN1Primitive;
|
|
||||||
import org.bouncycastle.asn1.ASN1Sequence;
|
|
||||||
import org.bouncycastle.asn1.DEROctetString;
|
|
||||||
import org.bouncycastle.asn1.DERPrintableString;
|
|
||||||
import org.bouncycastle.asn1.DERSequence;
|
|
||||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FilterInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.Signature;
|
|
||||||
import java.security.cert.CertificateEncodingException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class SignBoot {
|
|
||||||
|
|
||||||
private static final int BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET = 1632;
|
|
||||||
private static final int BOOT_IMAGE_HEADER_V2_DTB_SIZE_OFFSET = 1648;
|
|
||||||
|
|
||||||
// Arbitrary maximum header version value; when greater assume the field is dt/extra size
|
|
||||||
private static final int BOOT_IMAGE_HEADER_VERSION_MAXIMUM = 8;
|
|
||||||
|
|
||||||
// Maximum header size byte value to read (currently the bootimg minimum page size)
|
|
||||||
private static final int BOOT_IMAGE_HEADER_SIZE_MAXIMUM = 2048;
|
|
||||||
|
|
||||||
private static class PushBackRWStream extends FilterInputStream {
|
|
||||||
private OutputStream out;
|
|
||||||
private int pos = 0;
|
|
||||||
private byte[] backBuf;
|
|
||||||
|
|
||||||
PushBackRWStream(InputStream in, OutputStream o) {
|
|
||||||
super(in);
|
|
||||||
out = o;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
int b;
|
|
||||||
if (backBuf != null && backBuf.length > pos) {
|
|
||||||
b = backBuf[pos++];
|
|
||||||
} else {
|
|
||||||
b = super.read();
|
|
||||||
out.write(b);
|
|
||||||
}
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] bytes, int off, int len) throws IOException {
|
|
||||||
int read = 0;
|
|
||||||
if (backBuf != null && backBuf.length > pos) {
|
|
||||||
read = Math.min(len, backBuf.length - pos);
|
|
||||||
System.arraycopy(backBuf, pos, bytes, off, read);
|
|
||||||
pos += read;
|
|
||||||
off += read;
|
|
||||||
len -= read;
|
|
||||||
}
|
|
||||||
if (len > 0) {
|
|
||||||
int ar = super.read(bytes, off, len);
|
|
||||||
read += ar;
|
|
||||||
out.write(bytes, off, ar);
|
|
||||||
}
|
|
||||||
return read;
|
|
||||||
}
|
|
||||||
|
|
||||||
void unread(byte[] buf) {
|
|
||||||
backBuf = buf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int fullRead(InputStream in, byte[] b) throws IOException {
|
|
||||||
return fullRead(in, b, 0, b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int fullRead(InputStream in, byte[] b, int off, int len) throws IOException {
|
|
||||||
int n = 0;
|
|
||||||
while (n < len) {
|
|
||||||
int count = in.read(b, off + n, len - n);
|
|
||||||
if (count <= 0)
|
|
||||||
break;
|
|
||||||
n += count;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean doSignature(
|
|
||||||
@Nullable X509Certificate cert, @Nullable PrivateKey key,
|
|
||||||
@NonNull InputStream imgIn, @NonNull OutputStream imgOut, @NonNull String target
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
PushBackRWStream in = new PushBackRWStream(imgIn, imgOut);
|
|
||||||
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
|
|
||||||
// First read the header
|
|
||||||
fullRead(in, hdr);
|
|
||||||
int signableSize = getSignableImageSize(hdr);
|
|
||||||
// Unread header
|
|
||||||
in.unread(hdr);
|
|
||||||
BootSignature bootsig = new BootSignature(target, signableSize);
|
|
||||||
if (cert == null) {
|
|
||||||
cert = CryptoUtils.readCertificate(
|
|
||||||
new ByteArrayInputStream(KeyData.verityCert()));
|
|
||||||
}
|
|
||||||
bootsig.setCertificate(cert);
|
|
||||||
if (key == null) {
|
|
||||||
key = CryptoUtils.readPrivateKey(
|
|
||||||
new ByteArrayInputStream(KeyData.verityKey()));
|
|
||||||
}
|
|
||||||
byte[] sig = bootsig.sign(key, in, signableSize);
|
|
||||||
bootsig.setSignature(sig, CryptoUtils.getSignatureAlgorithmIdentifier(key));
|
|
||||||
byte[] encoded_bootsig = bootsig.getEncoded();
|
|
||||||
imgOut.write(encoded_bootsig);
|
|
||||||
imgOut.flush();
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean verifySignature(InputStream imgIn, X509Certificate cert) {
|
|
||||||
try {
|
|
||||||
// Read the header for size
|
|
||||||
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
|
|
||||||
if (fullRead(imgIn, hdr) != hdr.length) {
|
|
||||||
System.err.println("Unable to read image header");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int signableSize = getSignableImageSize(hdr);
|
|
||||||
|
|
||||||
// Read the rest of the image
|
|
||||||
byte[] rawImg = Arrays.copyOf(hdr, signableSize);
|
|
||||||
int remain = signableSize - hdr.length;
|
|
||||||
if (fullRead(imgIn, rawImg, hdr.length, remain) != remain) {
|
|
||||||
System.err.println("Unable to read image");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read footer, which contains the signature
|
|
||||||
byte[] signature = new byte[4096];
|
|
||||||
if (imgIn.read(signature) == -1 || Arrays.equals(signature, new byte [signature.length])) {
|
|
||||||
System.err.println("Invalid image: not signed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
BootSignature bootsig = new BootSignature(signature);
|
|
||||||
if (cert != null) {
|
|
||||||
bootsig.setCertificate(cert);
|
|
||||||
}
|
|
||||||
if (bootsig.verify(rawImg, signableSize)) {
|
|
||||||
System.err.println("Signature is VALID");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
System.err.println("Signature is INVALID");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getSignableImageSize(byte[] data) throws Exception {
|
|
||||||
if (!Arrays.equals(Arrays.copyOfRange(data, 0, 8),
|
|
||||||
"ANDROID!".getBytes("US-ASCII"))) {
|
|
||||||
throw new IllegalArgumentException("Invalid image header: missing magic");
|
|
||||||
}
|
|
||||||
ByteBuffer image = ByteBuffer.wrap(data);
|
|
||||||
image.order(ByteOrder.LITTLE_ENDIAN);
|
|
||||||
image.getLong(); // magic
|
|
||||||
int kernelSize = image.getInt();
|
|
||||||
image.getInt(); // kernel_addr
|
|
||||||
int ramdskSize = image.getInt();
|
|
||||||
image.getInt(); // ramdisk_addr
|
|
||||||
int secondSize = image.getInt();
|
|
||||||
image.getLong(); // second_addr + tags_addr
|
|
||||||
int pageSize = image.getInt();
|
|
||||||
if (pageSize >= 0x02000000) {
|
|
||||||
throw new IllegalArgumentException("Invalid image header: PXA header detected");
|
|
||||||
}
|
|
||||||
int length = pageSize // include the page aligned image header
|
|
||||||
+ ((kernelSize + pageSize - 1) / pageSize) * pageSize
|
|
||||||
+ ((ramdskSize + pageSize - 1) / pageSize) * pageSize
|
|
||||||
+ ((secondSize + pageSize - 1) / pageSize) * pageSize;
|
|
||||||
int headerVersion = image.getInt(); // boot image header version or dt/extra size
|
|
||||||
if (headerVersion > 0 && headerVersion < BOOT_IMAGE_HEADER_VERSION_MAXIMUM) {
|
|
||||||
image.position(BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET);
|
|
||||||
int recoveryDtboLength = image.getInt();
|
|
||||||
length += ((recoveryDtboLength + pageSize - 1) / pageSize) * pageSize;
|
|
||||||
image.getLong(); // recovery_dtbo address
|
|
||||||
int headerSize = image.getInt();
|
|
||||||
if (headerVersion == 2) {
|
|
||||||
image.position(BOOT_IMAGE_HEADER_V2_DTB_SIZE_OFFSET);
|
|
||||||
int dtbLength = image.getInt();
|
|
||||||
length += ((dtbLength + pageSize - 1) / pageSize) * pageSize;
|
|
||||||
image.getLong(); // dtb address
|
|
||||||
}
|
|
||||||
if (image.position() != headerSize) {
|
|
||||||
throw new IllegalArgumentException("Invalid image header: invalid header length");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// headerVersion is 0 or actually dt/extra size in this case
|
|
||||||
length += ((headerVersion + pageSize - 1) / pageSize) * pageSize;
|
|
||||||
}
|
|
||||||
length = ((length + pageSize - 1) / pageSize) * pageSize;
|
|
||||||
if (length <= 0) {
|
|
||||||
throw new IllegalArgumentException("Invalid image header: invalid length");
|
|
||||||
}
|
|
||||||
return length;
|
|
||||||
}
|
|
||||||
|
|
||||||
static class BootSignature extends ASN1Object {
|
|
||||||
private ASN1Integer formatVersion;
|
|
||||||
private ASN1Encodable certificate;
|
|
||||||
private AlgorithmIdentifier algId;
|
|
||||||
private DERPrintableString target;
|
|
||||||
private ASN1Integer length;
|
|
||||||
private DEROctetString signature;
|
|
||||||
private PublicKey publicKey;
|
|
||||||
private static final int FORMAT_VERSION = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the object for signing an image file
|
|
||||||
* @param target Target name, included in the signed data
|
|
||||||
* @param length Length of the image, included in the signed data
|
|
||||||
*/
|
|
||||||
public BootSignature(String target, int length) {
|
|
||||||
this.formatVersion = new ASN1Integer(FORMAT_VERSION);
|
|
||||||
this.target = new DERPrintableString(target);
|
|
||||||
this.length = new ASN1Integer(length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the object for verifying a signed image file
|
|
||||||
* @param signature Signature footer
|
|
||||||
*/
|
|
||||||
public BootSignature(byte[] signature) throws Exception {
|
|
||||||
ASN1InputStream stream = new ASN1InputStream(signature);
|
|
||||||
ASN1Sequence sequence = (ASN1Sequence) stream.readObject();
|
|
||||||
formatVersion = (ASN1Integer) sequence.getObjectAt(0);
|
|
||||||
if (formatVersion.getValue().intValue() != FORMAT_VERSION) {
|
|
||||||
throw new IllegalArgumentException("Unsupported format version");
|
|
||||||
}
|
|
||||||
certificate = sequence.getObjectAt(1);
|
|
||||||
byte[] encoded = ((ASN1Object) certificate).getEncoded();
|
|
||||||
ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
|
|
||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
|
||||||
X509Certificate c = (X509Certificate) cf.generateCertificate(bis);
|
|
||||||
publicKey = c.getPublicKey();
|
|
||||||
ASN1Sequence algId = (ASN1Sequence) sequence.getObjectAt(2);
|
|
||||||
this.algId = new AlgorithmIdentifier((ASN1ObjectIdentifier) algId.getObjectAt(0));
|
|
||||||
ASN1Sequence attrs = (ASN1Sequence) sequence.getObjectAt(3);
|
|
||||||
target = (DERPrintableString) attrs.getObjectAt(0);
|
|
||||||
length = (ASN1Integer) attrs.getObjectAt(1);
|
|
||||||
this.signature = (DEROctetString) sequence.getObjectAt(4);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ASN1Object getAuthenticatedAttributes() {
|
|
||||||
ASN1EncodableVector attrs = new ASN1EncodableVector();
|
|
||||||
attrs.add(target);
|
|
||||||
attrs.add(length);
|
|
||||||
return new DERSequence(attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getEncodedAuthenticatedAttributes() throws IOException {
|
|
||||||
return getAuthenticatedAttributes().getEncoded();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSignature(byte[] sig, AlgorithmIdentifier algId) {
|
|
||||||
this.algId = algId;
|
|
||||||
signature = new DEROctetString(sig);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCertificate(X509Certificate cert)
|
|
||||||
throws CertificateEncodingException, IOException {
|
|
||||||
ASN1InputStream s = new ASN1InputStream(cert.getEncoded());
|
|
||||||
certificate = s.readObject();
|
|
||||||
publicKey = cert.getPublicKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] sign(PrivateKey key, InputStream is, int len) throws Exception {
|
|
||||||
Signature signer = Signature.getInstance(CryptoUtils.getSignatureAlgorithm(key));
|
|
||||||
signer.initSign(key);
|
|
||||||
int read;
|
|
||||||
byte buffer[] = new byte[4096];
|
|
||||||
while ((read = is.read(buffer, 0, Math.min(len, buffer.length))) > 0) {
|
|
||||||
signer.update(buffer, 0, read);
|
|
||||||
len -= read;
|
|
||||||
}
|
|
||||||
signer.update(getEncodedAuthenticatedAttributes());
|
|
||||||
return signer.sign();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean verify(byte[] image, int length) throws Exception {
|
|
||||||
if (this.length.getValue().intValue() != length) {
|
|
||||||
throw new IllegalArgumentException("Invalid image length");
|
|
||||||
}
|
|
||||||
String algName = CryptoUtils.ID_TO_ALG.get(algId.getAlgorithm().getId());
|
|
||||||
if (algName == null) {
|
|
||||||
throw new IllegalArgumentException("Unsupported algorithm " + algId.getAlgorithm());
|
|
||||||
}
|
|
||||||
Signature verifier = Signature.getInstance(algName);
|
|
||||||
verifier.initVerify(publicKey);
|
|
||||||
verifier.update(image, 0, length);
|
|
||||||
verifier.update(getEncodedAuthenticatedAttributes());
|
|
||||||
return verifier.verify(signature.getOctets());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ASN1Primitive toASN1Primitive() {
|
|
||||||
ASN1EncodableVector v = new ASN1EncodableVector();
|
|
||||||
v.add(formatVersion);
|
|
||||||
v.add(certificate);
|
|
||||||
v.add(algId);
|
|
||||||
v.add(getAuthenticatedAttributes());
|
|
||||||
v.add(signature);
|
|
||||||
return new DERSequence(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
if (args.length > 0 && "-verify".equals(args[0])) {
|
|
||||||
X509Certificate cert = null;
|
|
||||||
if (args.length >= 2) {
|
|
||||||
// args[1] is the path to a public key certificate
|
|
||||||
cert = CryptoUtils.readCertificate(new FileInputStream(args[1]));
|
|
||||||
}
|
|
||||||
boolean signed = SignBoot.verifySignature(System.in, cert);
|
|
||||||
System.exit(signed ? 0 : 1);
|
|
||||||
} else if (args.length > 0 && "-sign".equals(args[0])) {
|
|
||||||
X509Certificate cert = null;
|
|
||||||
PrivateKey key = null;
|
|
||||||
String name = "/boot";
|
|
||||||
|
|
||||||
if (args.length >= 3) {
|
|
||||||
cert = CryptoUtils.readCertificate(new FileInputStream(args[1]));
|
|
||||||
key = CryptoUtils.readPrivateKey(new FileInputStream(args[2]));
|
|
||||||
}
|
|
||||||
if (args.length == 2) {
|
|
||||||
name = args[1];
|
|
||||||
} else if (args.length >= 4) {
|
|
||||||
name = args[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean result = SignBoot.doSignature(cert, key, System.in, System.out, name);
|
|
||||||
System.exit(result ? 0 : 1);
|
|
||||||
} else {
|
|
||||||
System.err.println(
|
|
||||||
"BootSigner <actions> [args]\n" +
|
|
||||||
"Input from stdin, output to stdout\n" +
|
|
||||||
"\n" +
|
|
||||||
"Actions:\n" +
|
|
||||||
" -verify [x509.pem]\n" +
|
|
||||||
" verify image. cert is optional.\n" +
|
|
||||||
" -sign [x509.pem] [pk8] [name]\n" +
|
|
||||||
" sign image. name and the cert/key pair are optional.\n" +
|
|
||||||
" name should be either /boot (default) or /recovery.\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.topjohnwu.magisk.ui
|
package com.topjohnwu.magisk.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -14,6 +16,7 @@ import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.MainDirections
|
import com.topjohnwu.magisk.MainDirections
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||||
|
import com.topjohnwu.magisk.arch.startAnimations
|
||||||
import com.topjohnwu.magisk.arch.viewModel
|
import com.topjohnwu.magisk.arch.viewModel
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
@ -21,9 +24,7 @@ import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||||
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
|
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
|
||||||
import com.topjohnwu.magisk.ktx.startAnimations
|
|
||||||
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -52,11 +53,19 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
||||||
|
|
||||||
private var isRootFragment = true
|
private var isRootFragment = true
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
override fun showMainUI(savedInstanceState: Bundle?) {
|
override fun showMainUI(savedInstanceState: Bundle?) {
|
||||||
setContentView()
|
setContentView()
|
||||||
showUnsupportedMessage()
|
showUnsupportedMessage()
|
||||||
askForHomeShortcut()
|
askForHomeShortcut()
|
||||||
|
|
||||||
|
// Ask permission to post notifications for background update check
|
||||||
|
if (Config.checkUpdate) {
|
||||||
|
withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||||
|
Config.checkUpdate = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||||
|
|
||||||
navigation.addOnDestinationChangedListener { _, destination, _ ->
|
navigation.addOnDestinationChangedListener { _, destination, _ ->
|
||||||
|
@ -88,7 +97,7 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
||||||
// https://issuetracker.google.com/issues/124538620
|
// https://issuetracker.google.com/issues/124538620
|
||||||
}
|
}
|
||||||
binding.mainNavigation.menu.apply {
|
binding.mainNavigation.menu.apply {
|
||||||
findItem(R.id.superuserFragment)?.isEnabled = Utils.showSuperUser()
|
findItem(R.id.superuserFragment)?.isEnabled = Info.showSuperUser
|
||||||
findItem(R.id.modulesFragment)?.isEnabled = Info.env.isActive && LocalModule.loaded()
|
findItem(R.id.modulesFragment)?.isEnabled = Info.env.isActive && LocalModule.loaded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,37 +6,46 @@ import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.arch.NavigationActivity
|
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.JobService
|
import com.topjohnwu.magisk.core.JobService
|
||||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
|
import com.topjohnwu.magisk.core.ktx.toast
|
||||||
|
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
@SuppressLint("CustomSplashScreen")
|
@SuppressLint("CustomSplashScreen")
|
||||||
abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var skipSplash = false
|
private var splashShown = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var needShowMainUI = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
setTheme(Theme.selected.themeRes)
|
setTheme(Theme.selected.themeRes)
|
||||||
|
|
||||||
if (isRunningAsStub && !skipSplash) {
|
if (isRunningAsStub && !splashShown) {
|
||||||
// Manually apply splash theme for stub
|
// Manually apply splash theme for stub
|
||||||
theme.applyStyle(R.style.StubSplashTheme, true)
|
theme.applyStyle(R.style.StubSplashTheme, true)
|
||||||
}
|
}
|
||||||
|
@ -45,22 +54,27 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||||
|
|
||||||
if (!isRunningAsStub) {
|
if (!isRunningAsStub) {
|
||||||
val splashScreen = installSplashScreen()
|
val splashScreen = installSplashScreen()
|
||||||
splashScreen.setKeepOnScreenCondition { !skipSplash }
|
splashScreen.setKeepOnScreenCondition { !splashShown }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skipSplash) {
|
if (splashShown) {
|
||||||
showMainUI(savedInstanceState)
|
doShowMainUI(savedInstanceState)
|
||||||
} else {
|
} else {
|
||||||
Shell.getShell(Shell.EXECUTOR) {
|
Shell.getShell(Shell.EXECUTOR) {
|
||||||
if (isRunningAsStub && !it.isRoot) {
|
if (isRunningAsStub && !it.isRoot) {
|
||||||
showInvalidStateMessage()
|
showInvalidStateMessage()
|
||||||
return@getShell
|
return@getShell
|
||||||
}
|
}
|
||||||
preLoad(savedInstanceState)
|
initialize(savedInstanceState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun doShowMainUI(savedInstanceState: Bundle?) {
|
||||||
|
needShowMainUI = false
|
||||||
|
showMainUI(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
abstract fun showMainUI(savedInstanceState: Bundle?)
|
abstract fun showMainUI(savedInstanceState: Bundle?)
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
|
@ -73,7 +87,7 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||||
onClick {
|
onClick {
|
||||||
withPermission(REQUEST_INSTALL_PACKAGES) {
|
withPermission(REQUEST_INSTALL_PACKAGES) {
|
||||||
if (!it) {
|
if (!it) {
|
||||||
Utils.toast(R.string.install_unknown_denied, Toast.LENGTH_SHORT)
|
toast(R.string.install_unknown_denied, Toast.LENGTH_SHORT)
|
||||||
showInvalidStateMessage()
|
showInvalidStateMessage()
|
||||||
} else {
|
} else {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
@ -88,7 +102,14 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preLoad(savedState: Bundle?) {
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (needShowMainUI) {
|
||||||
|
doShowMainUI(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialize(savedState: Bundle?) {
|
||||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)?.let {
|
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)?.let {
|
||||||
// Make sure the calling package matches (prevent DoS)
|
// Make sure the calling package matches (prevent DoS)
|
||||||
if (it == realCallingPackage)
|
if (it == realCallingPackage)
|
||||||
|
@ -98,7 +119,21 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.load(prevPkg)
|
Config.load(prevPkg)
|
||||||
handleRepackage(prevPkg)
|
|
||||||
|
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 = ""
|
||||||
|
if (prevPkg != null) {
|
||||||
|
Shell.cmd("(pm uninstall $prevPkg)& >/dev/null 2>&1").exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (prevPkg != null) {
|
if (prevPkg != null) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
// Relaunch the process after package migration
|
// Relaunch the process after package migration
|
||||||
|
@ -107,7 +142,31 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifications.setup(this)
|
// Validate stub APK
|
||||||
|
if (isRunningAsStub && (
|
||||||
|
// Version mismatch
|
||||||
|
Info.stub!!.version != BuildConfig.STUB_VERSION ||
|
||||||
|
// Not properly patched
|
||||||
|
intent.component!!.className.contains(HideAPK.PLACEHOLDER)
|
||||||
|
)) {
|
||||||
|
withPermission(REQUEST_INSTALL_PACKAGES) { granted ->
|
||||||
|
if (granted) {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val apk = File(cacheDir, "stub.apk")
|
||||||
|
try {
|
||||||
|
assets.open("stub.apk").writeTo(apk)
|
||||||
|
HideAPK.upgrade(this@SplashActivity, apk)?.let {
|
||||||
|
startActivity(it)
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
JobService.schedule(this)
|
JobService.schedule(this)
|
||||||
Shortcuts.setupDynamic(this)
|
Shortcuts.setupDynamic(this)
|
||||||
|
|
||||||
|
@ -118,29 +177,17 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
||||||
RootUtils.Connection.await()
|
RootUtils.Connection.await()
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
skipSplash = true
|
splashShown = true
|
||||||
if (isRunningAsStub) {
|
if (isRunningAsStub) {
|
||||||
// Re-launch main activity without splash theme
|
// Re-launch main activity without splash theme
|
||||||
relaunch()
|
relaunch()
|
||||||
} else {
|
} else {
|
||||||
showMainUI(savedState)
|
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||||
|
doShowMainUI(savedState)
|
||||||
|
} else {
|
||||||
|
needShowMainUI = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (!Const.Version.atLeast_25_0() && Config.suManager.isNotEmpty())
|
|
||||||
Config.suManager = ""
|
|
||||||
pkg ?: return
|
|
||||||
Shell.cmd("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,20 @@ import android.annotation.SuppressLint
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.ComponentInfo
|
import android.content.pm.ComponentInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.*
|
import android.content.pm.PackageManager.GET_ACTIVITIES
|
||||||
|
import android.content.pm.PackageManager.GET_PROVIDERS
|
||||||
|
import android.content.pm.PackageManager.GET_RECEIVERS
|
||||||
|
import android.content.pm.PackageManager.GET_SERVICES
|
||||||
|
import android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS
|
||||||
|
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
import android.content.pm.ServiceInfo
|
import android.content.pm.ServiceInfo
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import androidx.core.os.ProcessCompat
|
import androidx.core.os.ProcessCompat
|
||||||
|
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
import com.topjohnwu.magisk.ktx.getLabel
|
import java.util.TreeSet
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class CmdlineListItem(line: String) {
|
class CmdlineListItem(line: String) {
|
||||||
val packageName: String
|
val packageName: String
|
||||||
|
@ -67,7 +73,8 @@ class AppProcessInfo(
|
||||||
val proc = info.processName ?: info.packageName
|
val proc = info.processName ?: info.packageName
|
||||||
createProcess("${proc}_zygote")
|
createProcess("${proc}_zygote")
|
||||||
} else {
|
} else {
|
||||||
val proc = if (SDK_INT >= 29) "${it.getProcName()}:${it.name}" else it.getProcName()
|
val proc = if (SDK_INT >= Build.VERSION_CODES.Q)
|
||||||
|
"${it.getProcName()}:${it.name}" else it.getProcName()
|
||||||
createProcess(proc, ISOLATED_MAGIC)
|
createProcess(proc, ISOLATED_MAGIC)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,33 +1,32 @@
|
||||||
package com.topjohnwu.magisk.ui.deny
|
package com.topjohnwu.magisk.ui.deny
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseFragment
|
import com.topjohnwu.magisk.arch.BaseFragment
|
||||||
import com.topjohnwu.magisk.arch.viewModel
|
import com.topjohnwu.magisk.arch.viewModel
|
||||||
|
import com.topjohnwu.magisk.core.ktx.hideKeyboard
|
||||||
import com.topjohnwu.magisk.databinding.FragmentDenyMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentDenyMd2Binding
|
||||||
import com.topjohnwu.magisk.ktx.hideKeyboard
|
|
||||||
import rikka.recyclerview.addEdgeSpacing
|
import rikka.recyclerview.addEdgeSpacing
|
||||||
import rikka.recyclerview.addItemSpacing
|
import rikka.recyclerview.addItemSpacing
|
||||||
import rikka.recyclerview.fixEdgeEffect
|
import rikka.recyclerview.fixEdgeEffect
|
||||||
|
|
||||||
class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>(), MenuProvider {
|
||||||
|
|
||||||
override val layoutRes = R.layout.fragment_deny_md2
|
override val layoutRes = R.layout.fragment_deny_md2
|
||||||
override val viewModel by viewModel<DenyListViewModel>()
|
override val viewModel by viewModel<DenyListViewModel>()
|
||||||
|
|
||||||
private lateinit var searchView: SearchView
|
private lateinit var searchView: SearchView
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onStart() {
|
||||||
super.onAttach(context)
|
super.onStart()
|
||||||
activity?.setTitle(R.string.denylist)
|
activity?.setTitle(R.string.denylist)
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
@ -56,7 +55,7 @@ class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
||||||
return super.onBackPressed()
|
return super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.menu_deny_md2, menu)
|
inflater.inflate(R.menu.menu_deny_md2, menu)
|
||||||
searchView = menu.findItem(R.id.action_search).actionView as SearchView
|
searchView = menu.findItem(R.id.action_search).actionView as SearchView
|
||||||
searchView.queryHint = searchView.context.getString(R.string.hide_filter_hint)
|
searchView.queryHint = searchView.context.getString(R.string.hide_filter_hint)
|
||||||
|
@ -73,7 +72,7 @@ class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_show_system -> {
|
R.id.action_show_system -> {
|
||||||
val check = !item.isChecked
|
val check = !item.isChecked
|
||||||
|
@ -91,7 +90,7 @@ class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareMenu(menu: Menu) {
|
||||||
val showSystem = menu.findItem(R.id.action_show_system)
|
val showSystem = menu.findItem(R.id.action_show_system)
|
||||||
val showOS = menu.findItem(R.id.action_show_OS)
|
val showOS = menu.findItem(R.id.action_show_OS)
|
||||||
showOS.isEnabled = showSystem.isChecked
|
showOS.isEnabled = showSystem.isChecked
|
||||||
|
|
|
@ -5,17 +5,17 @@ import android.view.ViewGroup
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.databinding.ComparableRv
|
import com.topjohnwu.magisk.arch.startAnimations
|
||||||
import com.topjohnwu.magisk.databinding.ObservableDiffRvItem
|
import com.topjohnwu.magisk.databinding.DiffItem
|
||||||
|
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||||
import com.topjohnwu.magisk.databinding.addOnPropertyChangedCallback
|
import com.topjohnwu.magisk.databinding.addOnPropertyChangedCallback
|
||||||
import com.topjohnwu.magisk.databinding.set
|
import com.topjohnwu.magisk.databinding.set
|
||||||
import com.topjohnwu.magisk.ktx.startAnimations
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class DenyListRvItem(
|
class DenyListRvItem(
|
||||||
val info: AppProcessInfo
|
val info: AppProcessInfo
|
||||||
) : ObservableDiffRvItem<DenyListRvItem>(), ComparableRv<DenyListRvItem> {
|
) : ObservableRvItem(), DiffItem<DenyListRvItem>, Comparable<DenyListRvItem> {
|
||||||
|
|
||||||
override val layoutRes get() = R.layout.item_hide_md2
|
override val layoutRes get() = R.layout.item_hide_md2
|
||||||
|
|
||||||
|
@ -44,9 +44,18 @@ class DenyListRvItem(
|
||||||
processes
|
processes
|
||||||
.filterNot { it.isEnabled }
|
.filterNot { it.isEnabled }
|
||||||
.filter { isExpanded || it.defaultSelection }
|
.filter { isExpanded || it.defaultSelection }
|
||||||
|
.forEach { it.toggle() }
|
||||||
} else {
|
} else {
|
||||||
processes.filter { it.isEnabled }
|
Shell.cmd("magisk --denylist rm ${info.packageName}").submit()
|
||||||
}.forEach { it.toggle() }
|
processes.filter { it.isEnabled }.forEach {
|
||||||
|
if (it.process.isIsolated) {
|
||||||
|
it.toggle()
|
||||||
|
} else {
|
||||||
|
it.isEnabled = !it.isEnabled
|
||||||
|
notifyPropertyChanged(BR.enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -91,7 +100,7 @@ class DenyListRvItem(
|
||||||
|
|
||||||
class ProcessRvItem(
|
class ProcessRvItem(
|
||||||
val process: ProcessInfo
|
val process: ProcessInfo
|
||||||
) : ObservableDiffRvItem<ProcessRvItem>() {
|
) : ObservableRvItem(), DiffItem<ProcessRvItem> {
|
||||||
|
|
||||||
override val layoutRes get() = R.layout.item_hide_process_md2
|
override val layoutRes get() = R.layout.item_hide_process_md2
|
||||||
|
|
||||||
|
@ -113,10 +122,9 @@ class ProcessRvItem(
|
||||||
val defaultSelection get() =
|
val defaultSelection get() =
|
||||||
process.isIsolated || process.isAppZygote || process.name == process.packageName
|
process.isIsolated || process.isAppZygote || process.name == process.packageName
|
||||||
|
|
||||||
override fun contentSameAs(other: ProcessRvItem) =
|
|
||||||
process.isEnabled == other.process.isEnabled
|
|
||||||
|
|
||||||
override fun itemSameAs(other: ProcessRvItem) =
|
override fun itemSameAs(other: ProcessRvItem) =
|
||||||
process.name == other.process.name && process.packageName == other.process.packageName
|
process.name == other.process.name && process.packageName == other.process.packageName
|
||||||
|
|
||||||
|
override fun contentSameAs(other: ProcessRvItem) =
|
||||||
|
process.isEnabled == other.process.isEnabled
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,14 @@ package com.topjohnwu.magisk.ui.deny
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||||
import com.topjohnwu.magisk.core.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.core.ktx.concurrentMap
|
||||||
import com.topjohnwu.magisk.databinding.bindExtra
|
import com.topjohnwu.magisk.databinding.bindExtra
|
||||||
import com.topjohnwu.magisk.databinding.filterableListOf
|
import com.topjohnwu.magisk.databinding.filterList
|
||||||
import com.topjohnwu.magisk.databinding.set
|
import com.topjohnwu.magisk.databinding.set
|
||||||
import com.topjohnwu.magisk.ktx.concurrentMap
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
@ -22,22 +23,22 @@ class DenyListViewModel : AsyncLoadViewModel() {
|
||||||
var isShowSystem = false
|
var isShowSystem = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
query()
|
doQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isShowOS = false
|
var isShowOS = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
query()
|
doQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = ""
|
var query = ""
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
query()
|
doQuery(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
val items = filterableListOf<DenyListRvItem>()
|
val items = filterList<DenyListRvItem>(viewModelScope)
|
||||||
val extraBindings = bindExtra {
|
val extraBindings = bindExtra {
|
||||||
it.put(BR.viewModel, this)
|
it.put(BR.viewModel, this)
|
||||||
}
|
}
|
||||||
|
@ -49,7 +50,7 @@ class DenyListViewModel : AsyncLoadViewModel() {
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
override suspend fun doLoadWork() {
|
override suspend fun doLoadWork() {
|
||||||
loading = true
|
loading = true
|
||||||
val (apps, diff) = withContext(Dispatchers.Default) {
|
val apps = withContext(Dispatchers.Default) {
|
||||||
val pm = AppContext.packageManager
|
val pm = AppContext.packageManager
|
||||||
val denyList = Shell.cmd("magisk --denylist ls").exec().out
|
val denyList = Shell.cmd("magisk --denylist ls").exec().out
|
||||||
.map { CmdlineListItem(it) }
|
.map { CmdlineListItem(it) }
|
||||||
|
@ -62,22 +63,22 @@ class DenyListViewModel : AsyncLoadViewModel() {
|
||||||
.toCollection(ArrayList(size))
|
.toCollection(ArrayList(size))
|
||||||
}
|
}
|
||||||
apps.sort()
|
apps.sort()
|
||||||
apps to items.calculateDiff(apps)
|
apps
|
||||||
}
|
}
|
||||||
items.update(apps, diff)
|
items.set(apps)
|
||||||
query()
|
doQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun query() {
|
private fun doQuery(s: String) {
|
||||||
items.filter {
|
items.filter {
|
||||||
fun filterSystem() = isShowSystem || !it.info.isSystemApp()
|
fun filterSystem() = isShowSystem || !it.info.isSystemApp()
|
||||||
|
|
||||||
fun filterOS() = (isShowSystem && isShowOS) || it.info.isApp()
|
fun filterOS() = (isShowSystem && isShowOS) || it.info.isApp()
|
||||||
|
|
||||||
fun filterQuery(): Boolean {
|
fun filterQuery(): Boolean {
|
||||||
fun inName() = it.info.label.contains(query, true)
|
fun inName() = it.info.label.contains(s, true)
|
||||||
fun inPackage() = it.info.packageName.contains(query, true)
|
fun inPackage() = it.info.packageName.contains(s, true)
|
||||||
fun inProcesses() = it.processes.any { p -> p.process.name.contains(query, true) }
|
fun inProcesses() = it.processes.any { p -> p.process.name.contains(s, true) }
|
||||||
return inName() || inPackage() || inProcesses()
|
return inName() || inPackage() || inProcesses()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,14 +6,15 @@ import androidx.core.view.updateLayoutParams
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.databinding.DiffRvItem
|
import com.topjohnwu.magisk.databinding.DiffItem
|
||||||
import com.topjohnwu.magisk.databinding.RvContainer
|
import com.topjohnwu.magisk.databinding.ItemWrapper
|
||||||
import com.topjohnwu.magisk.databinding.ViewAwareRvItem
|
import com.topjohnwu.magisk.databinding.RvItem
|
||||||
|
import com.topjohnwu.magisk.databinding.ViewAwareItem
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class ConsoleItem(
|
class ConsoleItem(
|
||||||
override val item: String
|
override val item: String
|
||||||
) : DiffRvItem<ConsoleItem>(), ViewAwareRvItem, RvContainer<String> {
|
) : RvItem(), ViewAwareItem, DiffItem<ConsoleItem>, ItemWrapper<String> {
|
||||||
override val layoutRes = R.layout.item_console_md2
|
override val layoutRes = R.layout.item_console_md2
|
||||||
|
|
||||||
private var parentWidth = -1
|
private var parentWidth = -1
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue