mirror of https://github.com/topjohnwu/Magisk
Compare commits
594 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 |
|
@ -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
|
|
@ -2,107 +2,153 @@ name: Magisk Build
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'native/**'
|
||||
- 'stub/**'
|
||||
- 'buildSrc/**'
|
||||
- 'build.py'
|
||||
- 'gradle.properties'
|
||||
- '.github/workflows/build.yml'
|
||||
- "app/**"
|
||||
- "native/**"
|
||||
- "stub/**"
|
||||
- "buildSrc/**"
|
||||
- "build.py"
|
||||
- "gradle.properties"
|
||||
- ".github/workflows/build.yml"
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: Build Magisk artifacts
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SCCACHE_DIRECT: false
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
env:
|
||||
NDK_CCACHE: ccache
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
|
||||
RUSTC_WRAPPER: sccache
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Set up ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
key: ${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}
|
||||
|
||||
- name: Set up sccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
variant: sccache
|
||||
key: ${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v3
|
||||
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@v3
|
||||
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
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build release
|
||||
run: |
|
||||
python build.py -vr all
|
||||
run: ./build.py -vr all
|
||||
|
||||
- name: Build debug
|
||||
run: |
|
||||
python build.py -v all
|
||||
run: ./build.py -v all
|
||||
|
||||
- name: Stop gradle daemon
|
||||
run: ./gradlew --stop
|
||||
|
||||
# Only upload artifacts built on Linux
|
||||
- name: Upload build artifact
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
||||
compression-level: 9
|
||||
|
||||
- name: Upload mapping and native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ github.sha }}-symbols
|
||||
path: app/build/outputs
|
||||
compression-level: 9
|
||||
|
||||
test-build:
|
||||
name: Test building on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
SCCACHE_DIRECT: false
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, macos-14]
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build debug
|
||||
run: python build.py -v all
|
||||
|
||||
- name: Stop gradle daemon
|
||||
run: ./gradlew --stop
|
||||
|
||||
test:
|
||||
name: Test x86_64 on API ${{ matrix.api }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
api: [23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34]
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
||||
|
||||
- name: Upload mapping and native debug symbols
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v3
|
||||
- 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:
|
||||
name: ${{ github.sha }}-symbols
|
||||
path: app/build/outputs
|
||||
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 }}
|
||||
|
|
|
@ -4,9 +4,6 @@
|
|||
[submodule "busybox"]
|
||||
path = native/src/external/busybox
|
||||
url = https://github.com/topjohnwu/ndk-busybox.git
|
||||
[submodule "dtc"]
|
||||
path = native/src/external/dtc
|
||||
url = https://github.com/dgibson/dtc.git
|
||||
[submodule "lz4"]
|
||||
path = native/src/external/lz4
|
||||
url = https://github.com/lz4/lz4.git
|
||||
|
@ -16,12 +13,6 @@
|
|||
[submodule "xz"]
|
||||
path = native/src/external/xz
|
||||
url = https://github.com/xz-mirror/xz.git
|
||||
[submodule "nanopb"]
|
||||
path = native/src/external/nanopb
|
||||
url = https://github.com/nanopb/nanopb.git
|
||||
[submodule "mincrypt"]
|
||||
path = native/src/external/mincrypt
|
||||
url = https://github.com/topjohnwu/mincrypt.git
|
||||
[submodule "pcre"]
|
||||
path = native/src/external/pcre
|
||||
url = https://android.googlesource.com/platform/external/pcre
|
||||
|
@ -31,9 +22,6 @@
|
|||
[submodule "zlib"]
|
||||
path = native/src/external/zlib
|
||||
url = https://android.googlesource.com/platform/external/zlib
|
||||
[submodule "parallel-hashmap"]
|
||||
path = native/src/external/parallel-hashmap
|
||||
url = https://github.com/greg7mdp/parallel-hashmap.git
|
||||
[submodule "zopfli"]
|
||||
path = native/src/external/zopfli
|
||||
url = https://github.com/google/zopfli.git
|
||||
|
@ -43,6 +31,12 @@
|
|||
[submodule "lsplt"]
|
||||
path = native/src/external/lsplt
|
||||
url = https://github.com/LSPosed/LSPlt.git
|
||||
[submodule "system_properties"]
|
||||
path = native/src/external/system_properties
|
||||
url = https://github.com/topjohnwu/system_properties.git
|
||||
[submodule "crt0"]
|
||||
path = native/src/external/crt0
|
||||
url = https://github.com/topjohnwu/crt0.git
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
|
|
29
README.MD
29
README.MD
|
@ -18,14 +18,15 @@ Some highlight features:
|
|||
|
||||
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
||||
|
||||
[![](https://img.shields.io/badge/Magisk-v25.2-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v25.2)
|
||||
[![](https://img.shields.io/badge/Magisk%20Beta-v25.2-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v25.2)
|
||||
[![](https://img.shields.io/badge/Magisk-v27.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v27.0)
|
||||
[![](https://img.shields.io/badge/Magisk%20Beta-v27.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v27.0)
|
||||
[![](https://img.shields.io/badge/Magisk-Canary-red)](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
||||
[![](https://img.shields.io/badge/Magisk-Debug-red)](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||
|
||||
## Useful Links
|
||||
|
||||
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
|
||||
- [Building and Development](https://topjohnwu.github.io/Magisk/build.html)
|
||||
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
|
||||
|
||||
## Bug Reports
|
||||
|
@ -36,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 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.8+ \
|
||||
(Windows only: select **'Add Python to PATH'** in installer, and run `pip install colorama` after install)
|
||||
- Configure to use the JDK bundled in Android Studio:
|
||||
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/Contents/Home"`
|
||||
- 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 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
|
||||
|
||||
Default string resources for the Magisk app and its stub APK are located here:
|
||||
|
|
|
@ -70,17 +70,17 @@ configurations.all {
|
|||
dependencies {
|
||||
implementation(project(":app:shared"))
|
||||
|
||||
implementation("com.github.topjohnwu:jtar:1.0.0")
|
||||
implementation("com.github.topjohnwu:jtar:1.1.0")
|
||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||
implementation("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||
implementation("org.bouncycastle:bcpkix-jdk18on:1.77")
|
||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
|
||||
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
|
||||
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
|
||||
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.2")
|
||||
implementation("io.noties.markwon:core:4.6.2")
|
||||
|
||||
val vLibsu = "5.0.5"
|
||||
val vLibsu = "5.2.2"
|
||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
||||
|
@ -90,32 +90,32 @@ dependencies {
|
|||
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
|
||||
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
|
||||
|
||||
val vOkHttp = "4.10.0"
|
||||
val vOkHttp = "4.12.0"
|
||||
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
|
||||
|
||||
val vMoshi = "1.14.0"
|
||||
val vMoshi = "1.15.0"
|
||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||
|
||||
val vRoom = "2.5.1"
|
||||
val vRoom = "2.6.1"
|
||||
implementation("androidx.room:room-runtime:${vRoom}")
|
||||
implementation("androidx.room:room-ktx:${vRoom}")
|
||||
kapt("androidx.room:room-compiler:${vRoom}")
|
||||
|
||||
val vNav = "2.5.3"
|
||||
val vNav = "2.7.7"
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.5.6")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
||||
implementation("androidx.transition:transition:1.4.1")
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0")
|
||||
implementation("com.google.android.material:material:1.8.0")
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
||||
implementation("com.google.android.material:material:1.11.0")
|
||||
}
|
||||
|
|
|
@ -4,11 +4,14 @@
|
|||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RUN_USER_INITIATED_JOBS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE"
|
||||
android:maxSdkVersion="33" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
|
|
|
@ -67,8 +67,6 @@ public final class APKInstall {
|
|||
public interface Session {
|
||||
// @WorkerThread
|
||||
OutputStream openStream(Context context) throws IOException;
|
||||
// @WorkerThread
|
||||
void install(Context context, File apk) throws IOException;
|
||||
// @WorkerThread @Nullable
|
||||
Intent waitIntent();
|
||||
}
|
||||
|
@ -167,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,8 +61,9 @@
|
|||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".core.download.DownloadService"
|
||||
android:name=".core.Service"
|
||||
android:exported="false"
|
||||
android:enabled="@bool/enable_fg_service"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
|
@ -82,11 +83,15 @@
|
|||
android:name="androidx.room.MultiInstanceInvalidationService"
|
||||
tools:node="remove" />
|
||||
|
||||
<!-- We don't need emoji compat -->
|
||||
<!-- We handle initialization ourselves -->
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="remove" />
|
||||
|
||||
<!-- We handle profile installation ourselves -->
|
||||
<receiver
|
||||
android:name="androidx.profileinstaller.ProfileInstallReceiver"
|
||||
tools:node="remove" />
|
||||
|
||||
</application>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package androidx.lifecycle;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class ProcessLifecycleAccessor {
|
||||
public static void init(@NonNull Context context) {
|
||||
LifecycleDispatcher.init(context);
|
||||
ProcessLifecycleOwner.init(context);
|
||||
}
|
||||
}
|
|
@ -10,9 +10,9 @@ import androidx.databinding.DataBindingUtil
|
|||
import androidx.databinding.OnRebindCallback
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.ktx.startAnimations
|
||||
|
||||
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
|
||||
|
||||
|
@ -38,6 +38,9 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||
it.setVariable(BR.viewModel, viewModel)
|
||||
it.lifecycleOwner = viewLifecycleOwner
|
||||
}
|
||||
if (this is MenuProvider) {
|
||||
activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
|
||||
}
|
||||
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
||||
return binding.root
|
||||
}
|
||||
|
@ -67,8 +70,6 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if (this is MenuProvider)
|
||||
activity?.addMenuProvider(this, viewLifecycleOwner)
|
||||
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
|
||||
override fun onPreBind(binding: Binding): Boolean {
|
||||
this@BaseFragment.onPreBind(binding)
|
||||
|
@ -92,5 +93,4 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||
fun NavDirections.navigate() {
|
||||
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.Manifest.permission.*
|
||||
import android.Manifest.permission.POST_NOTIFICATIONS
|
||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
|
|
|
@ -5,13 +5,18 @@ import android.graphics.Color
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.res.use
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import rikka.insets.WindowInsetsHelper
|
||||
|
@ -97,3 +102,14 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun ViewGroup.startAnimations() {
|
||||
val transition = AutoTransition()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.setDuration(400)
|
||||
.excludeTarget(R.id.main_toolbar, true)
|
||||
TransitionManager.beginDelayedTransition(
|
||||
this,
|
||||
transition
|
||||
)
|
||||
}
|
||||
|
|
|
@ -34,7 +34,8 @@ object VMFactory : ViewModelProvider.Factory {
|
|||
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
||||
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
||||
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
||||
InstallViewModel::class.java -> InstallViewModel(ServiceLocator.networkService)
|
||||
InstallViewModel::class.java ->
|
||||
InstallViewModel(ServiceLocator.networkService, ServiceLocator.markwon)
|
||||
SuRequestViewModel::class.java ->
|
||||
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
||||
else -> modelClass.newInstance()
|
||||
|
|
|
@ -5,10 +5,14 @@ import android.app.Application
|
|||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.ProcessLifecycleAccessor
|
||||
import android.system.Os
|
||||
import androidx.profileinstaller.ProfileInstaller
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.DispatcherExecutor
|
||||
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
||||
import com.topjohnwu.magisk.core.utils.ProcessLifecycle
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.core.utils.ShellInit
|
||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||
|
@ -19,6 +23,8 @@ import com.topjohnwu.superuser.Shell
|
|||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.system.exitProcess
|
||||
|
@ -41,6 +47,8 @@ open class App() : Application() {
|
|||
Timber.e(e)
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
Os.setenv("PATH", "${Os.getenv("PATH")}:/debug_ramdisk:/sbin", true)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(context: Context) {
|
||||
|
@ -81,7 +89,13 @@ open class App() : Application() {
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ProcessLifecycleAccessor.init(this)
|
||||
ProcessLifecycle.init(this)
|
||||
NetworkObserver.init(this)
|
||||
if (!BuildConfig.DEBUG && !isRunningAsStub) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ProfileInstaller.writeProfile(this@App)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Xml
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.repository.BoolDBPropertyNoWrite
|
||||
import com.topjohnwu.magisk.core.repository.DBConfig
|
||||
import com.topjohnwu.magisk.core.repository.PreferenceConfig
|
||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.IOException
|
||||
|
||||
object Config : PreferenceConfig, DBConfig {
|
||||
|
||||
|
@ -25,13 +25,12 @@ object Config : PreferenceConfig, DBConfig {
|
|||
override val context get() = ServiceLocator.deContext
|
||||
override val coroutineScope get() = GlobalScope
|
||||
|
||||
@get:SuppressLint("ApplySharedPref")
|
||||
val prefsFile: File get() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().apply {
|
||||
remove(Key.ASKED_HOME)
|
||||
}.commit()
|
||||
return File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||
private val prefsFile = File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
fun getPrefsFile(): File {
|
||||
prefs.edit().remove(Key.ASKED_HOME).commit()
|
||||
return prefsFile
|
||||
}
|
||||
|
||||
object Key {
|
||||
|
@ -41,6 +40,7 @@ object Config : PreferenceConfig, DBConfig {
|
|||
const val SU_MNT_NS = "mnt_ns"
|
||||
const val SU_BIOMETRIC = "su_biometric"
|
||||
const val ZYGISK = "zygisk"
|
||||
const val BOOTLOOP = "bootloop"
|
||||
const val DENYLIST = "denylist"
|
||||
const val SU_MANAGER = "requester"
|
||||
const val KEYSTORE = "keystore"
|
||||
|
@ -118,7 +118,6 @@ object Config : PreferenceConfig, DBConfig {
|
|||
|
||||
@JvmField var keepVerity = false
|
||||
@JvmField var keepEnc = false
|
||||
@JvmField var patchVbmeta = false
|
||||
@JvmField var recovery = false
|
||||
|
||||
var bootId by preference(Key.BOOT_ID, "")
|
||||
|
@ -161,8 +160,14 @@ object Config : PreferenceConfig, DBConfig {
|
|||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||
private var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||
var userAuth
|
||||
get() = Info.isDeviceSecure && suBiometric
|
||||
set(value) {
|
||||
suBiometric = value
|
||||
}
|
||||
var zygisk by dbSettings(Key.ZYGISK, false)
|
||||
var bootloop by dbSettings(Key.BOOTLOOP, 0)
|
||||
var denyList by BoolDBPropertyNoWrite(Key.DENYLIST, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||
|
@ -171,10 +176,15 @@ object Config : PreferenceConfig, DBConfig {
|
|||
|
||||
fun load(pkg: String?) {
|
||||
// Only try to load prefs when fresh install and a previous package name is set
|
||||
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
||||
context.contentResolver.openInputStream(Provider.preferencesUri(pkg))?.use {
|
||||
prefs.edit { parsePrefs(it) }
|
||||
if (pkg != null && prefs.all.isEmpty()) {
|
||||
runBlocking {
|
||||
try {
|
||||
context.contentResolver
|
||||
.openInputStream(Provider.preferencesUri(pkg))
|
||||
?.writeTo(prefsFile, dispatcher = Dispatchers.Unconfined)
|
||||
} catch (ignored: IOException) {}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
prefs.edit {
|
||||
|
@ -191,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,8 @@ object Const {
|
|||
}
|
||||
|
||||
object ID {
|
||||
const val JOB_SERVICE_ID = 7
|
||||
const val DOWNLOAD_JOB_ID = 6
|
||||
const val CHECK_UPDATE_JOB_ID = 7
|
||||
}
|
||||
|
||||
object Url {
|
||||
|
|
|
@ -13,8 +13,8 @@ import android.util.DisplayMetrics
|
|||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.unwrap
|
||||
import com.topjohnwu.magisk.core.utils.syncLocale
|
||||
import com.topjohnwu.magisk.ktx.unwrap
|
||||
|
||||
lateinit var AppApkPath: String
|
||||
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.LiveData
|
||||
import android.app.KeyguardManager
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.getProperty
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.core.utils.NetworkObserver
|
||||
import com.topjohnwu.magisk.ktx.getProperty
|
||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||
|
||||
val isRunningAsStub get() = Info.stub != null
|
||||
|
@ -28,30 +26,31 @@ object Info {
|
|||
// Device state
|
||||
@JvmStatic val env by lazy { loadState() }
|
||||
@JvmField var isSAR = false
|
||||
var legacySAR = false
|
||||
var isAB = false
|
||||
@JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
|
||||
@JvmStatic val isFDE get() = crypto == "block"
|
||||
@JvmField var ramdisk = false
|
||||
@JvmField var vbmeta = false
|
||||
var patchBootVbmeta = false
|
||||
var crypto = ""
|
||||
var noDataExec = false
|
||||
var isRooted = false
|
||||
|
||||
@JvmField var hasGMS = true
|
||||
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||
@JvmField val isEmulator =
|
||||
getProperty("ro.kernel.qemu", "0") == "1" ||
|
||||
getProperty("ro.boot.qemu", "0") == "1"
|
||||
|
||||
val isConnected: LiveData<Boolean> by lazy {
|
||||
MutableLiveData(false).also { field ->
|
||||
NetworkObserver.observe(AppContext) {
|
||||
remote = EMPTY_REMOTE
|
||||
field.postValue(it)
|
||||
}
|
||||
}
|
||||
val isConnected = MutableLiveData(false)
|
||||
|
||||
val showSuperUser: Boolean get() {
|
||||
return env.isActive && (Const.USER_ID == 0
|
||||
|| Config.suMultiuserMode == Config.Value.MULTIUSER_MODE_USER)
|
||||
}
|
||||
|
||||
val isDeviceSecure get() =
|
||||
AppContext.getSystemService(KeyguardManager::class.java).isDeviceSecure
|
||||
|
||||
private fun loadState(): Env {
|
||||
val v = fastCmd("magisk -v").split(":".toRegex())
|
||||
return Env(
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Notification
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobParameters
|
||||
import android.app.job.JobScheduler
|
||||
|
@ -8,38 +11,78 @@ import androidx.core.content.getSystemService
|
|||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.base.BaseJobService
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class JobService : BaseJobService() {
|
||||
|
||||
private val job = Job()
|
||||
private val svc get() = ServiceLocator.networkService
|
||||
private var mSession: Session? = null
|
||||
|
||||
override fun onStartJob(params: JobParameters): Boolean {
|
||||
val coroutineScope = CoroutineScope(Dispatchers.IO + job)
|
||||
coroutineScope.launch {
|
||||
doWork()
|
||||
@TargetApi(value = 34)
|
||||
inner class Session(
|
||||
private var params: JobParameters
|
||||
) : DownloadEngine.Session {
|
||||
|
||||
override val context get() = this@JobService
|
||||
val engine = DownloadEngine(this)
|
||||
|
||||
fun updateParams(params: JobParameters) {
|
||||
this.params = params
|
||||
engine.reattach()
|
||||
}
|
||||
|
||||
override fun attachNotification(id: Int, builder: Notification.Builder) {
|
||||
setNotification(params, id, builder.build(), JOB_END_NOTIFICATION_POLICY_REMOVE)
|
||||
}
|
||||
|
||||
override fun onDownloadComplete() {
|
||||
jobFinished(params, false)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun doWork() {
|
||||
svc.fetchUpdate()?.let {
|
||||
Info.remote = it
|
||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
||||
Notifications.updateAvailable()
|
||||
@SuppressLint("NewApi")
|
||||
override fun onStartJob(params: JobParameters): Boolean {
|
||||
return when (params.jobId) {
|
||||
Const.ID.CHECK_UPDATE_JOB_ID -> checkUpdate(params)
|
||||
Const.ID.DOWNLOAD_JOB_ID -> downloadFile(params)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters): Boolean {
|
||||
job.cancel()
|
||||
return false
|
||||
override fun onStopJob(params: JobParameters?) = false
|
||||
|
||||
@TargetApi(value = 34)
|
||||
private fun downloadFile(params: JobParameters): Boolean {
|
||||
params.transientExtras.classLoader = Subject::class.java.classLoader
|
||||
val subject = params.transientExtras
|
||||
.getParcelable(DownloadEngine.SUBJECT_KEY, Subject::class.java) ?:
|
||||
return false
|
||||
|
||||
val session = mSession?.also {
|
||||
it.updateParams(params)
|
||||
} ?: run {
|
||||
Session(params).also { mSession = it }
|
||||
}
|
||||
|
||||
session.engine.download(subject)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun checkUpdate(params: JobParameters): Boolean {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ServiceLocator.networkService.fetchUpdate()?.let {
|
||||
Info.remote = it
|
||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
||||
Notifications.updateAvailable()
|
||||
jobFinished(params, false)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -47,14 +90,14 @@ class JobService : BaseJobService() {
|
|||
val scheduler = context.getSystemService<JobScheduler>() ?: return
|
||||
if (Config.checkUpdate) {
|
||||
val cmp = JobService::class.java.cmp(context.packageName)
|
||||
val info = JobInfo.Builder(Const.ID.JOB_SERVICE_ID, cmp)
|
||||
val info = JobInfo.Builder(Const.ID.CHECK_UPDATE_JOB_ID, cmp)
|
||||
.setPeriodic(TimeUnit.HOURS.toMillis(12))
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build()
|
||||
scheduler.schedule(info)
|
||||
} else {
|
||||
scheduler.cancel(Const.ID.JOB_SERVICE_ID)
|
||||
scheduler.cancel(Const.ID.CHECK_UPDATE_JOB_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,17 +6,23 @@ import android.os.ParcelFileDescriptor
|
|||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.core.su.TestHandler
|
||||
|
||||
class Provider : BaseProvider() {
|
||||
|
||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
SuCallbackHandler.run(context!!, method, extras)
|
||||
return Bundle.EMPTY
|
||||
return when (method) {
|
||||
SuCallbackHandler.LOG, SuCallbackHandler.NOTIFY -> {
|
||||
SuCallbackHandler.run(context!!, method, extras)
|
||||
Bundle.EMPTY
|
||||
}
|
||||
else -> TestHandler.run(method)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
return when (uri.encodedPath ?: return null) {
|
||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.getPrefsFile(), MODE_READ_ONLY)
|
||||
else -> super.openFile(uri, mode)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,11 @@ package com.topjohnwu.magisk.core
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.IntentCompat
|
||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
@ -35,6 +38,12 @@ open class Receiver : BaseReceiver() {
|
|||
}
|
||||
|
||||
when (intent.action ?: return) {
|
||||
DownloadEngine.ACTION -> {
|
||||
IntentCompat.getParcelableExtra(
|
||||
intent, DownloadEngine.SUBJECT_KEY, Subject::class.java)?.let {
|
||||
DownloadEngine.start(context, it)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
// This will only work pre-O
|
||||
if (Config.suReAuth)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -18,10 +18,11 @@ import androidx.activity.result.contract.ActivityResultContracts.RequestPermissi
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.reflectField
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.utils.RequestAuthentication
|
||||
import com.topjohnwu.magisk.core.utils.RequestInstall
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.ktx.reflectField
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
|
||||
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
||||
fun onActivityLaunch() {}
|
||||
|
@ -43,6 +44,12 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||
installCallback = null
|
||||
}
|
||||
|
||||
var authenticateCallback: ((Boolean) -> Unit)? = null
|
||||
val requestAuthenticate = registerForActivityResult(RequestAuthentication()) {
|
||||
authenticateCallback?.invoke(it)
|
||||
authenticateCallback = null
|
||||
}
|
||||
|
||||
private var contentCallback: ContentResultCallback? = null
|
||||
private val getContent = registerForActivityResult(GetContent()) {
|
||||
if (it != null) contentCallback?.onActivityResult(it)
|
||||
|
@ -110,7 +117,7 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||
getContent.launch(type)
|
||||
callback.onActivityLaunch()
|
||||
} 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.UpdateInfo
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.*
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Streaming
|
||||
import retrofit2.http.Url
|
||||
|
||||
private const val BRANCH = "branch"
|
||||
private const val REPO = "repo"
|
||||
|
|
|
@ -1,15 +1,31 @@
|
|||
package com.topjohnwu.magisk.core.data
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Database
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
|
||||
@Database(version = 1, entities = [SuLog::class], exportSchema = false)
|
||||
@Database(version = 2, entities = [SuLog::class], exportSchema = false)
|
||||
abstract class SuLogDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun suLogDao(): SuLogDao
|
||||
|
||||
companion object {
|
||||
val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) = with(database) {
|
||||
execSQL("ALTER TABLE logs ADD COLUMN target INTEGER NOT NULL DEFAULT -1")
|
||||
execSQL("ALTER TABLE logs ADD COLUMN context TEXT NOT NULL DEFAULT ''")
|
||||
execSQL("ALTER TABLE logs ADD COLUMN gids TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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 kotlinx.coroutines.Dispatchers
|
||||
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.SettingsDao
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||
|
||||
|
@ -45,6 +45,7 @@ object ServiceLocator {
|
|||
|
||||
private fun createSuLogDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||
.addMigrations(SuLogDatabase.MIGRATION_1_2)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
||||
|
|
|
@ -0,0 +1,373 @@
|
|||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.app.job.JobInfo
|
||||
import android.app.job.JobScheduler
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.collection.isNotEmpty
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.ActivityTracker
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.JobService
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.cmp
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.core.ktx.forEach
|
||||
import com.topjohnwu.magisk.core.ktx.set
|
||||
import com.topjohnwu.magisk.core.ktx.withStreams
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.ResponseBody
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
/**
|
||||
* This class drives the execution of file downloads and notification management.
|
||||
*
|
||||
* Each download engine instance has to be paired with a "session" that is managed by the operating
|
||||
* system. A session is an Android component that allows executing long lasting operations and
|
||||
* have its state tied to a notification to show progress.
|
||||
*
|
||||
* A session can only have one single notification representing its state, and the operating system
|
||||
* also uses the notification to manage the lifecycle of a session. One goal of this class is
|
||||
* to support concurrent download tasks using only one single session, so internally it manages
|
||||
* all active tasks and notifications and properly re-assign notifications to be attached to
|
||||
* the session to make sure all download operations can be completed without the operating system
|
||||
* killing the session.
|
||||
*
|
||||
* For API 23 - 33, we use a foreground service as a session.
|
||||
* For API 34 and higher, we use user-initiated job services as a session.
|
||||
*/
|
||||
class DownloadEngine(
|
||||
private val session: Session
|
||||
) {
|
||||
|
||||
interface Session {
|
||||
val context: Context
|
||||
|
||||
fun attachNotification(id: Int, builder: Notification.Builder)
|
||||
fun onDownloadComplete()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION = "com.topjohnwu.magisk.DOWNLOAD"
|
||||
const val SUBJECT_KEY = "subject"
|
||||
private const val REQUEST_CODE = 1
|
||||
|
||||
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
||||
|
||||
private fun broadcast(progress: Float, subject: Subject) {
|
||||
progressBroadcast.postValue(progress to subject)
|
||||
}
|
||||
|
||||
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
||||
progressBroadcast.value = null
|
||||
progressBroadcast.observe(owner) {
|
||||
val (progress, subject) = it ?: return@observe
|
||||
callback(progress, subject)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createIntent(context: Context, subject: Subject) =
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
context.intent<com.topjohnwu.magisk.core.Receiver>()
|
||||
.setAction(ACTION)
|
||||
.putExtra(SUBJECT_KEY, subject)
|
||||
} else {
|
||||
context.intent<com.topjohnwu.magisk.core.Service>()
|
||||
.setAction(ACTION)
|
||||
.putExtra(SUBJECT_KEY, subject)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
|
||||
val flag = PendingIntent.FLAG_IMMUTABLE or
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or
|
||||
PendingIntent.FLAG_ONE_SHOT
|
||||
val intent = createIntent(context, subject)
|
||||
return if (Build.VERSION.SDK_INT >= 34) {
|
||||
// On API 34+, download tasks are handled with a user-initiated job.
|
||||
// However, there is no way to schedule a new job directly with a pending intent.
|
||||
// As a workaround, we send the subject to a broadcast receiver and have it
|
||||
// schedule the job for us.
|
||||
PendingIntent.getBroadcast(context, REQUEST_CODE, intent, flag)
|
||||
} else if (Build.VERSION.SDK_INT >= 26) {
|
||||
PendingIntent.getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||
} else {
|
||||
PendingIntent.getService(context, REQUEST_CODE, intent, flag)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun startWithActivity(activity: BaseActivity, subject: Subject) {
|
||||
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||
// Always download regardless of notification permission status
|
||||
start(activity.applicationContext, subject)
|
||||
}
|
||||
}
|
||||
|
||||
fun start(context: Context, subject: Subject) {
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
val scheduler = context.getSystemService<JobScheduler>()!!
|
||||
val cmp = JobService::class.java.cmp(context.packageName)
|
||||
val extras = Bundle()
|
||||
extras.putParcelable(SUBJECT_KEY, subject)
|
||||
val info = JobInfo.Builder(Const.ID.DOWNLOAD_JOB_ID, cmp)
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
.setUserInitiated(true)
|
||||
.setTransientExtras(extras)
|
||||
.build()
|
||||
scheduler.schedule(info)
|
||||
} else if (Build.VERSION.SDK_INT >= 26) {
|
||||
context.startForegroundService(createIntent(context, subject))
|
||||
} else {
|
||||
context.startService(createIntent(context, subject))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun download(subject: Subject) {
|
||||
notifyUpdate(subject.notifyId)
|
||||
CoroutineScope(job + Dispatchers.IO).launch {
|
||||
try {
|
||||
val stream = network.fetchFile(subject.url).toProgressStream(subject)
|
||||
when (subject) {
|
||||
is Subject.App -> handleApp(stream, subject)
|
||||
is Subject.Module -> handleModule(stream, subject.file)
|
||||
else -> stream.copyAndClose(subject.file.outputStream())
|
||||
}
|
||||
val activity = ActivityTracker.foreground
|
||||
if (activity != null && subject.autoLaunch) {
|
||||
notifyRemove(subject.notifyId)
|
||||
subject.pendingIntent(activity)?.send()
|
||||
} else {
|
||||
notifyFinish(subject)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
notifyFail(subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun reattach() {
|
||||
val builder = notifications[attachedId] ?: return
|
||||
session.attachNotification(attachedId, builder)
|
||||
}
|
||||
|
||||
private val notifications = SparseArrayCompat<Notification.Builder>()
|
||||
private var attachedId = -1
|
||||
|
||||
private val job = Job()
|
||||
|
||||
private val context get() = session.context
|
||||
private val network get() = ServiceLocator.networkService
|
||||
|
||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||
val notification = notifyRemove(id)?.also(editor) ?: return -1
|
||||
val newId = Notifications.nextId()
|
||||
Notifications.mgr.notify(newId, notification.build())
|
||||
return newId
|
||||
}
|
||||
|
||||
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
|
||||
broadcast(-2f, subject)
|
||||
it.setContentText(context.getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
|
||||
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
|
||||
broadcast(1f, subject)
|
||||
it.setContentTitle(subject.title)
|
||||
.setContentText(context.getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
subject.pendingIntent(context)?.let { intent -> it.setContentIntent(intent) }
|
||||
}
|
||||
|
||||
private fun attachNotification(id: Int, notification: Notification.Builder) {
|
||||
attachedId = id
|
||||
session.attachNotification(id, notification)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
val notification = (notifications[id] ?: Notifications.startProgress("").also {
|
||||
notifications[id] = it
|
||||
}).apply(editor)
|
||||
|
||||
if (attachedId < 0)
|
||||
attachNotification(id, notification)
|
||||
else
|
||||
Notifications.mgr.notify(id, notification.build())
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun notifyRemove(id: Int): Notification.Builder? {
|
||||
val idx = notifications.indexOfKey(id)
|
||||
var n: Notification.Builder? = null
|
||||
|
||||
if (idx >= 0) {
|
||||
n = notifications.valueAt(idx)
|
||||
notifications.removeAt(idx)
|
||||
|
||||
// The cancelled notification is the one attached to the session, need special handling
|
||||
if (attachedId == id) {
|
||||
if (notifications.isNotEmpty()) {
|
||||
// There are still remaining notifications, pick one and attach to the session
|
||||
val anotherId = notifications.keyAt(0)
|
||||
val notification = notifications.valueAt(0)
|
||||
attachNotification(anotherId, notification)
|
||||
} else {
|
||||
// No more notifications left, terminate the session
|
||||
attachedId = -1
|
||||
session.onDownloadComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.mgr.cancel(id)
|
||||
return n
|
||||
}
|
||||
|
||||
private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||
val external = subject.file.outputStream()
|
||||
|
||||
if (isRunningAsStub) {
|
||||
val updateApk = StubApk.update(context)
|
||||
try {
|
||||
// Download full APK to stub update path
|
||||
stream.copyAndClose(TeeOutputStream(external, updateApk.outputStream()))
|
||||
|
||||
// Also upgrade stub
|
||||
notifyUpdate(subject.notifyId) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setContentTitle(context.getString(R.string.hide_app_title))
|
||||
.setContentText("")
|
||||
}
|
||||
|
||||
// Extract stub
|
||||
val zf = ZipFile(updateApk)
|
||||
val apk = context.cachedFile("stub.apk")
|
||||
apk.delete()
|
||||
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||
zf.close()
|
||||
|
||||
// Patch and install
|
||||
subject.intent = HideAPK.upgrade(context, apk)
|
||||
?: throw IOException("HideAPK patch error")
|
||||
apk.delete()
|
||||
} catch (e: Exception) {
|
||||
// If any error occurred, do not let stub load the new APK
|
||||
updateApk.delete()
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
val session = APKInstall.startSession(context)
|
||||
stream.copyAndClose(TeeOutputStream(external, session.openStream(context)))
|
||||
subject.intent = session.waitIntent()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleModule(src: InputStream, file: Uri) {
|
||||
val input = ZipInputStream(src)
|
||||
val output = ZipOutputStream(file.outputStream())
|
||||
|
||||
withStreams(input, output) { zin, zout ->
|
||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||
context.assets.open("module_installer.sh").use { it.copyAll(zout) }
|
||||
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||
zout.write("#MAGISK\n".toByteArray())
|
||||
|
||||
zin.forEach { entry ->
|
||||
val path = entry.name
|
||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||
zout.putNextEntry(ZipEntry(path))
|
||||
if (!entry.isDirectory) {
|
||||
zin.copyAll(zout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TeeOutputStream(
|
||||
private val o1: OutputStream,
|
||||
private val o2: OutputStream
|
||||
) : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
o1.write(b)
|
||||
o2.write(b)
|
||||
}
|
||||
override fun write(b: ByteArray?, off: Int, len: Int) {
|
||||
o1.write(b, off, len)
|
||||
o2.write(b, off, len)
|
||||
}
|
||||
override fun close() {
|
||||
o1.close()
|
||||
o2.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
||||
val max = contentLength()
|
||||
val total = max.toFloat() / 1048576
|
||||
val id = subject.notifyId
|
||||
|
||||
notifyUpdate(id) { it.setContentTitle(subject.title) }
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it.toFloat() / 1048576
|
||||
notifyUpdate(id) { notification ->
|
||||
if (max > 0) {
|
||||
broadcast(progress / total, subject)
|
||||
notification
|
||||
.setProgress(max.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, total))
|
||||
} else {
|
||||
broadcast(-1f, subject)
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.Manifest
|
||||
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.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.ktx.*
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.Properties
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
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() {
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun download(subject: Subject) {
|
||||
notifyUpdate(subject.notifyId)
|
||||
CoroutineScope(job + Dispatchers.IO).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) {
|
||||
notifyRemove(subject.notifyId)
|
||||
subject.pendingIntent(activity)?.send()
|
||||
} else {
|
||||
notifyFinish(subject)
|
||||
}
|
||||
subject.postDownload?.invoke()
|
||||
if (!hasNotifications)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
notifyFail(subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private 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())
|
||||
|
||||
val zf = ZipFile(updateApk)
|
||||
val prop = Properties()
|
||||
prop.load(ByteArrayInputStream(zf.comment.toByteArray()))
|
||||
val stubVersion = prop.getProperty("stubVersion").toIntOrNull() ?: -1
|
||||
if (Info.stub!!.version < stubVersion) {
|
||||
// Also upgrade stub
|
||||
notifyUpdate(subject.notifyId) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_app_title))
|
||||
.setContentText("")
|
||||
}
|
||||
|
||||
// Extract stub
|
||||
val apk = subject.file.toFile()
|
||||
zf.getInputStream(zf.getEntry("assets/stub.apk")).writeTo(apk)
|
||||
zf.close()
|
||||
|
||||
// Patch and install
|
||||
subject.intent = HideAPK.upgrade(this, apk)
|
||||
?: throw IOException("HideAPK patch error")
|
||||
apk.delete()
|
||||
} 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 = selfLaunchIntent()
|
||||
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 >= Build.VERSION_CODES.O) {
|
||||
getForegroundService(context, REQUEST_CODE, intent, flag)
|
||||
} else {
|
||||
getService(context, REQUEST_CODE, intent, flag)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun start(activity: BaseActivity, subject: Subject) {
|
||||
activity.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||
// Always download regardless of notification permission status
|
||||
val app = activity.applicationContext
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
app.startForegroundService(intent(app, subject))
|
||||
} else {
|
||||
app.startService(intent(app, subject))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
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
|
||||
|
||||
private var attachedNotificationId = 0
|
||||
|
||||
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
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 attachNotification(id: Int, notification: Notification) {
|
||||
attachedNotificationId = id
|
||||
startForeground(id, notification)
|
||||
}
|
||||
|
||||
private fun maybeDetachNotification(id: Int) : Boolean {
|
||||
if (attachedNotificationId != id) return false
|
||||
if (hasNotifications) {
|
||||
val (anotherId, notification) = notifications.entries.first()
|
||||
// Attaching a new notification will remove the current showing one
|
||||
attachNotification(anotherId, notification.build())
|
||||
return true
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
}
|
||||
attachedNotificationId = 0
|
||||
return true
|
||||
}
|
||||
|
||||
protected fun notifyUpdate(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
fun create() = Notifications.startProgress("")
|
||||
|
||||
val wasEmpty = !hasNotifications
|
||||
val notification = notifications.getOrPut(id, ::create).also(editor).build()
|
||||
if (wasEmpty)
|
||||
attachNotification(id, notification)
|
||||
else
|
||||
Notifications.mgr.notify(id, notification)
|
||||
}
|
||||
|
||||
protected fun notifyRemove(id: Int): Notification.Builder? {
|
||||
val n = notifications.remove(id)
|
||||
if (n == null || !maybeDetachNotification(id))
|
||||
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,21 +9,16 @@ import android.os.Parcelable
|
|||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri()
|
||||
|
||||
enum class Action {
|
||||
Flash,
|
||||
Download
|
||||
}
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
sealed class Subject : Parcelable {
|
||||
|
||||
|
@ -32,19 +27,17 @@ sealed class Subject : Parcelable {
|
|||
abstract val title: String
|
||||
abstract val notifyId: Int
|
||||
open val autoLaunch: Boolean get() = true
|
||||
open val postDownload: (() -> Unit)? get() = null
|
||||
|
||||
abstract fun pendingIntent(context: Context): PendingIntent?
|
||||
open fun pendingIntent(context: Context): PendingIntent? = null
|
||||
|
||||
@Parcelize
|
||||
class Module(
|
||||
val module: OnlineModule,
|
||||
val action: Action,
|
||||
private val module: OnlineModule,
|
||||
override val autoLaunch: Boolean,
|
||||
override val notifyId: Int = Notifications.nextId()
|
||||
) : Subject() {
|
||||
override val url: String get() = module.zipUrl
|
||||
override val title: String get() = module.downloadFilename
|
||||
override val autoLaunch: Boolean get() = action == Action.Flash
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
|
@ -65,17 +58,24 @@ sealed class Subject : Parcelable {
|
|||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
cachedFile("manager.apk")
|
||||
MediaStoreUtils.getFile("${title}.apk").uri
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
override var postDownload: (() -> Unit)? = null
|
||||
|
||||
@IgnoredOnParcel
|
||||
var intent: Intent? = null
|
||||
override fun pendingIntent(context: Context) = intent?.toPending(context)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class Test(
|
||||
override val notifyId: Int = Notifications.nextId(),
|
||||
override val title: String = UUID.randomUUID().toString().substring(0, 6)
|
||||
) : Subject() {
|
||||
override val url get() = "https://link.testfile.org/250MB"
|
||||
override val file get() = File("/dev/null").toUri()
|
||||
override val autoLaunch get() = false
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
protected fun Intent.toPending(context: Context): PendingIntent {
|
||||
return PendingIntent.getActivity(context, notifyId, this,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package com.topjohnwu.magisk.ktx
|
||||
package com.topjohnwu.magisk.core.ktx
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
|
@ -6,36 +6,26 @@ import android.content.*
|
|||
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
|
||||
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 android.widget.Toast
|
||||
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.magisk.utils.APKInstall
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
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)
|
||||
|
||||
|
@ -61,110 +51,6 @@ val Context.deviceProtectedContext: Context get() =
|
|||
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 {
|
||||
|
@ -188,10 +74,6 @@ fun Context.unwrap(): Context {
|
|||
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>()
|
||||
|
@ -199,17 +81,6 @@ fun Activity.hideKeyboard() {
|
|||
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) {
|
||||
|
@ -271,3 +142,11 @@ fun Context.selfLaunchIntent(): Intent {
|
|||
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 com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Collections
|
||||
import java.util.zip.ZipEntry
|
||||
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) {
|
||||
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.Const
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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()
|
||||
}
|
||||
|
||||
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() }
|
|
@ -4,7 +4,7 @@ import android.content.pm.PackageInfo
|
|||
import android.content.pm.PackageManager
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
|
||||
@Entity(tableName = "logs")
|
||||
class SuLog(
|
||||
|
@ -14,7 +14,10 @@ class SuLog(
|
|||
val packageName: String,
|
||||
val appName: String,
|
||||
val command: String,
|
||||
val action: Boolean,
|
||||
val action: Int,
|
||||
val target: Int,
|
||||
val context: String,
|
||||
val gids: String,
|
||||
val time: Long = System.currentTimeMillis()
|
||||
) {
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||
|
@ -25,7 +28,10 @@ fun PackageManager.createSuLog(
|
|||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String,
|
||||
policy: Int
|
||||
policy: Int,
|
||||
target: Int,
|
||||
context: String,
|
||||
gids: String,
|
||||
): SuLog {
|
||||
val appInfo = info.applicationInfo
|
||||
return SuLog(
|
||||
|
@ -35,7 +41,10 @@ fun PackageManager.createSuLog(
|
|||
packageName = getNameForUid(appInfo.uid)!!,
|
||||
appName = appInfo.getLabel(this),
|
||||
command = command,
|
||||
action = policy == SuPolicy.ALLOW
|
||||
action = policy,
|
||||
target = target,
|
||||
context = context,
|
||||
gids = gids,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -44,7 +53,10 @@ fun createSuLog(
|
|||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String,
|
||||
policy: Int
|
||||
policy: Int,
|
||||
target: Int,
|
||||
context: String,
|
||||
gids: String,
|
||||
): SuLog {
|
||||
return SuLog(
|
||||
fromUid = fromUid,
|
||||
|
@ -53,6 +65,9 @@ fun createSuLog(
|
|||
packageName = "[UID] $fromUid",
|
||||
appName = "[UID] $fromUid",
|
||||
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.Info
|
||||
import com.topjohnwu.magisk.core.data.SuLogDao
|
||||
import com.topjohnwu.magisk.core.ktx.await
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
|
||||
|
|
|
@ -7,11 +7,11 @@ import com.topjohnwu.magisk.BuildConfig
|
|||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import com.topjohnwu.magisk.core.ktx.getPackageInfo
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
import com.topjohnwu.magisk.ktx.getPackageInfo
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -57,17 +57,20 @@ object SuCallbackHandler {
|
|||
val toUid = data.getIntComp("to.uid", -1)
|
||||
val pid = data.getIntComp("pid", -1)
|
||||
val command = data.getString("command", "")
|
||||
val target = data.getIntComp("target", -1)
|
||||
val seContext = data.getString("context", "")
|
||||
val gids = data.getString("gids", "")
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val log = runCatching {
|
||||
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)
|
||||
notify(context, log.action, log.appName)
|
||||
notify(context, log.action == SuPolicy.ALLOW, log.appName)
|
||||
|
||||
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||
}
|
||||
|
@ -93,7 +96,7 @@ object SuCallbackHandler {
|
|||
else
|
||||
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.core.Config
|
||||
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.ktx.getPackageInfo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.DataOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -22,7 +23,7 @@ class SuRequestHandler(
|
|||
private val policyDB: PolicyDao
|
||||
) {
|
||||
|
||||
private lateinit var output: DataOutputStream
|
||||
private lateinit var output: File
|
||||
private lateinit var policy: SuPolicy
|
||||
lateinit var pkgInfo: PackageInfo
|
||||
private set
|
||||
|
@ -52,37 +53,32 @@ class SuRequestHandler(
|
|||
return true
|
||||
}
|
||||
|
||||
private fun close() {
|
||||
if (::output.isInitialized)
|
||||
runCatching { output.close() }
|
||||
}
|
||||
|
||||
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
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
|
||||
private suspend fun init(intent: Intent): Boolean {
|
||||
val uid = intent.getIntExtra("uid", -1)
|
||||
val pid = intent.getIntExtra("pid", -1)
|
||||
val fifo = intent.getStringExtra("fifo")
|
||||
if (uid <= 0 || pid <= 0 || fifo == null) {
|
||||
Timber.e("Unexpected extras: uid=[${uid}], pid=[${pid}], fifo=[${fifo}]")
|
||||
return false
|
||||
}
|
||||
output = File(fifo)
|
||||
policy = SuPolicy(uid)
|
||||
try {
|
||||
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
||||
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||
// We only fill in sharedUserId and leave other fields uninitialized
|
||||
sharedUserId = name.split(":")[0]
|
||||
}
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
Timber.e(e)
|
||||
respond(SuPolicy.DENY, -1)
|
||||
return false
|
||||
}
|
||||
if (!output.canWrite()) {
|
||||
Timber.e("Cannot write to $output")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun respond(action: Int, time: Int) {
|
||||
|
@ -97,14 +93,15 @@ class SuRequestHandler(
|
|||
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
output.writeInt(policy.policy)
|
||||
output.flush()
|
||||
DataOutputStream(FileOutputStream(output)).use {
|
||||
it.writeInt(policy.policy)
|
||||
it.flush()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
close()
|
||||
if (until >= 0)
|
||||
policyDB.update(policy)
|
||||
}
|
||||
if (until >= 0) {
|
||||
policyDB.update(policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.os.Bundle
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.NOPList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
object TestHandler {
|
||||
|
||||
fun run(method: String): Bundle {
|
||||
val r = Bundle()
|
||||
|
||||
fun setup(): Boolean {
|
||||
val nop = NOPList.getInstance()
|
||||
return runBlocking {
|
||||
MagiskInstaller.Emulator(nop, nop).exec()
|
||||
}
|
||||
}
|
||||
|
||||
fun test(): Boolean {
|
||||
// Make sure Zygisk works correctly
|
||||
if (!Info.isZygiskEnabled) {
|
||||
r.putString("reason", "zygisk not enabled")
|
||||
return false
|
||||
}
|
||||
|
||||
// Make sure the Magisk app can get root
|
||||
val shell = Shell.getShell()
|
||||
if (!shell.isRoot) {
|
||||
r.putString("reason", "shell not root")
|
||||
return false
|
||||
}
|
||||
|
||||
// Make sure the root service is running
|
||||
RootUtils.Connection.await()
|
||||
|
||||
// Clear existing grant for ADB shell
|
||||
runBlocking {
|
||||
ServiceLocator.policyDB.delete(2000)
|
||||
Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW
|
||||
Config.prefs.edit().commit()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val b = runCatching {
|
||||
when (method) {
|
||||
"setup" -> setup()
|
||||
"test" -> test()
|
||||
else -> {
|
||||
r.putString("reason", "unknown method")
|
||||
false
|
||||
}
|
||||
}
|
||||
}.getOrElse {
|
||||
r.putString("reason", it.stackTraceToString())
|
||||
false
|
||||
}
|
||||
|
||||
r.putBoolean("result", b)
|
||||
return r
|
||||
}
|
||||
}
|
|
@ -4,10 +4,10 @@ import android.net.Uri
|
|||
import androidx.core.net.toFile
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.unzip
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -26,7 +26,7 @@ open class FlashZip(
|
|||
private lateinit var zipFile: File
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun flash(): Boolean {
|
||||
private suspend fun flash(): Boolean {
|
||||
installDir.deleteRecursively()
|
||||
installDir.mkdirs()
|
||||
|
||||
|
@ -47,13 +47,13 @@ open class FlashZip(
|
|||
}
|
||||
}
|
||||
|
||||
val isValid = runCatching {
|
||||
val isValid = try {
|
||||
zipFile.unzip(installDir, "META-INF/com/google/android", true)
|
||||
val script = File(installDir, "updater-script")
|
||||
script.readText().contains("#MAGISK")
|
||||
}.getOrElse {
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unzip error")
|
||||
throw it
|
||||
throw e
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
|
|
|
@ -11,14 +11,15 @@ import com.topjohnwu.magisk.StubApk
|
|||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Provider
|
||||
import com.topjohnwu.magisk.core.ktx.await
|
||||
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.utils.AXML
|
||||
import com.topjohnwu.magisk.core.utils.Keygen
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.signing.JarMap
|
||||
import com.topjohnwu.magisk.signing.SignApk
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Runnable
|
||||
|
@ -168,7 +169,7 @@ object HideAPK {
|
|||
activity.finish()
|
||||
}
|
||||
|
||||
private 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")
|
||||
try {
|
||||
activity.assets.open("stub.apk").writeTo(stub)
|
||||
|
@ -195,7 +196,7 @@ object HideAPK {
|
|||
if (Shell.cmd(cmd).exec().isSuccess) return true
|
||||
|
||||
try {
|
||||
session.install(activity, repack)
|
||||
repack.inputStream().copyAndClose(session.openStream(activity))
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
|
@ -214,7 +215,7 @@ object HideAPK {
|
|||
}
|
||||
val onFailure = Runnable {
|
||||
dialog.dismiss()
|
||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
patchAndHide(activity, label, onFailure)
|
||||
|
@ -232,7 +233,7 @@ object HideAPK {
|
|||
}
|
||||
val onFailure = Runnable {
|
||||
dialog.dismiss()
|
||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
activity.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
val apk = StubApk.current(activity)
|
||||
val session = APKInstall.startSession(activity, APPLICATION_ID, onFailure) {
|
||||
|
@ -244,7 +245,7 @@ object HideAPK {
|
|||
if (Shell.cmd(cmd).await().isSuccess) return
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
session.install(activity, apk)
|
||||
apk.inputStream().copyAndClose(session.openStream(activity))
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return@withContext false
|
||||
|
|
|
@ -1,24 +1,32 @@
|
|||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Process
|
||||
import android.system.ErrnoException
|
||||
import android.system.Os
|
||||
import android.system.OsConstants
|
||||
import android.system.OsConstants.O_WRONLY
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.AppApkPath
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||
import com.topjohnwu.magisk.core.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.core.ktx.reboot
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.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.ShellUtils
|
||||
import com.topjohnwu.superuser.internal.NOPList
|
||||
|
@ -40,6 +48,7 @@ import java.util.*
|
|||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
abstract class MagiskInstallImpl protected constructor(
|
||||
protected val console: MutableList<String> = NOPList.getInstance(),
|
||||
|
@ -86,7 +95,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
return true
|
||||
}
|
||||
|
||||
private fun extractFiles(): Boolean {
|
||||
private suspend fun extractFiles(): Boolean {
|
||||
console.add("- Device platform: ${Const.CPU_ABI}")
|
||||
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
|
||||
|
@ -97,20 +106,23 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
try {
|
||||
// Extract binaries
|
||||
if (isRunningAsStub) {
|
||||
val zf = ZipFile(StubApk.current(context))
|
||||
ZipFile(StubApk.current(context)).use { zf ->
|
||||
zf.entries().asSequence().filter {
|
||||
!it.isDirectory && it.name.startsWith("/lib/${Const.CPU_ABI}/")
|
||||
}.forEach {
|
||||
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
|
||||
val name = n.substring(3, n.length - 3)
|
||||
val dest = File(installDir, name)
|
||||
zf.getInputStream(it).writeTo(dest)
|
||||
dest.setExecutable(true)
|
||||
}
|
||||
|
||||
// Also extract magisk32 on non 64-bit only 64-bit devices
|
||||
val is32lib = Const.CPU_ABI_32?.let {
|
||||
{ entry: ZipEntry -> entry.name == "lib/$it/libmagisk32.so" }
|
||||
} ?: { false }
|
||||
|
||||
zf.entries().asSequence().filter {
|
||||
!it.isDirectory && (it.name.startsWith("lib/${Const.CPU_ABI}/") || is32lib(it))
|
||||
}.forEach {
|
||||
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
|
||||
val name = n.substring(3, n.length - 3)
|
||||
val dest = File(installDir, name)
|
||||
zf.getInputStream(it).writeTo(dest)
|
||||
val abi32 = Const.CPU_ABI_32
|
||||
if (Process.is64Bit() && abi32 != null) {
|
||||
val magisk32 = File(installDir, "magisk32")
|
||||
zf.getInputStream(ZipEntry("lib/$abi32/libmagisk.so")).writeTo(magisk32)
|
||||
magisk32.setExecutable(true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val info = context.applicationInfo
|
||||
|
@ -118,16 +130,17 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
name.startsWith("lib") && name.endsWith(".so")
|
||||
} ?: emptyArray()
|
||||
|
||||
// Also symlink magisk32 on non 64-bit only 64-bit devices
|
||||
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir").get(info) as String?
|
||||
if (lib32 != null) {
|
||||
libs += File(lib32, "libmagisk32.so")
|
||||
}
|
||||
|
||||
for (lib in libs) {
|
||||
val name = lib.name.substring(3, lib.name.length - 3)
|
||||
Os.symlink(lib.path, "$installDir/$name")
|
||||
}
|
||||
|
||||
// Also symlink magisk32 on 64-bit devices that supports 32-bit
|
||||
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir")
|
||||
.get(info) as String?
|
||||
if (lib32 != null) {
|
||||
Os.symlink("$lib32/libmagisk.so", "$installDir/magisk32");
|
||||
}
|
||||
}
|
||||
|
||||
// Extract scripts
|
||||
|
@ -164,106 +177,221 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
return true
|
||||
}
|
||||
|
||||
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
|
||||
src.copyTo(out)
|
||||
}
|
||||
private suspend fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyAll(it) }
|
||||
|
||||
private fun newTarEntry(name: String, size: Long): TarEntry {
|
||||
console.add("-- Writing: $name")
|
||||
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
||||
}
|
||||
|
||||
private class NoAvailableStream(s: InputStream) : FilterInputStream(s) {
|
||||
// Make sure available is never called on the actual stream and always return 0
|
||||
// 1. Workaround bug in LZ4FrameInputStream
|
||||
// 2. Reduce max buffer size to prevent OOM
|
||||
override fun available() = 0
|
||||
}
|
||||
|
||||
private class NoBootException : IOException()
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun processTar(input: InputStream, output: OutputStream): OutputStream {
|
||||
private suspend fun processTar(tarIn: TarInputStream, tarOut: TarOutputStream): ExtendedFile {
|
||||
console.add("- Processing tar file")
|
||||
val tarOut = TarOutputStream(output)
|
||||
TarInputStream(input).use { tarIn ->
|
||||
lateinit var entry: TarEntry
|
||||
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 */ }
|
||||
}
|
||||
}
|
||||
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")
|
||||
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().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
|
||||
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)
|
||||
} else {
|
||||
console.add("-- Copying: ${entry.name}")
|
||||
tarOut.putNextEntry(entry)
|
||||
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
||||
}
|
||||
// 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")
|
||||
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)
|
||||
|
||||
suspend fun ExtendedFile.copyToTar() {
|
||||
newInputStream().use {
|
||||
tarOut.putNextEntry(newTarEntry(name, length()))
|
||||
it.copyAll(tarOut)
|
||||
}
|
||||
boot.delete()
|
||||
} else {
|
||||
srcBoot = when {
|
||||
initBoot.exists() -> initBoot
|
||||
boot.exists() -> boot
|
||||
else -> {
|
||||
console.add("! No boot image found")
|
||||
throw IOException()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
return tarOut
|
||||
if (boot.exists()) {
|
||||
return boot
|
||||
} else {
|
||||
throw NoBootException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFile(uri: Uri): Boolean {
|
||||
@Throws(IOException::class)
|
||||
private fun processPayload(input: InputStream): ExtendedFile {
|
||||
var fifo: File? = null
|
||||
try {
|
||||
console.add("- Processing payload.bin")
|
||||
fifo = File.createTempFile("payload-fifo-", null, installDir)
|
||||
fifo.delete()
|
||||
Os.mkfifo(fifo.path, 420 /* 0644 */)
|
||||
|
||||
// Enqueue the shell command first, or the subsequent FIFO open will block
|
||||
val future = arrayOf(
|
||||
"cd $installDir",
|
||||
"./magiskboot extract $fifo",
|
||||
"cd /"
|
||||
).eq()
|
||||
|
||||
val fd = Os.open(fifo.path, O_WRONLY, 0)
|
||||
try {
|
||||
val bufSize = 1024 * 1024
|
||||
val buf = ByteBuffer.allocate(bufSize)
|
||||
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
|
||||
while (buf.hasRemaining()) {
|
||||
try {
|
||||
Os.write(fd, buf)
|
||||
} catch (e: ErrnoException) {
|
||||
if (e.errno != OsConstants.EPIPE)
|
||||
throw e
|
||||
// If SIGPIPE, then the other side is closed, we're done
|
||||
break
|
||||
}
|
||||
if (!buf.hasRemaining()) {
|
||||
buf.limit(bufSize)
|
||||
buf.position(input.read(buf.array()).coerceAtLeast(0)).flip()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Os.close(fd)
|
||||
}
|
||||
|
||||
val success = try { future.get().isSuccess } catch (e: Exception) { false }
|
||||
if (!success) {
|
||||
console.add("! Error while extracting payload.bin")
|
||||
throw IOException()
|
||||
}
|
||||
val boot = installDir.getChildFile("boot.img")
|
||||
val initBoot = installDir.getChildFile("init_boot.img")
|
||||
return when {
|
||||
initBoot.exists() -> {
|
||||
console.add("-- Extract init_boot.img")
|
||||
initBoot
|
||||
}
|
||||
boot.exists() -> {
|
||||
console.add("-- Extract boot.img")
|
||||
boot
|
||||
}
|
||||
else -> {
|
||||
throw NoBootException()
|
||||
}
|
||||
}
|
||||
} catch (e: ErrnoException) {
|
||||
throw IOException(e)
|
||||
} finally {
|
||||
fifo?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleFile(uri: Uri): Boolean {
|
||||
val outStream: OutputStream
|
||||
var outFile: MediaStoreUtils.UriFile? = null
|
||||
val outFile: MediaStoreUtils.UriFile
|
||||
|
||||
// Process input file
|
||||
try {
|
||||
uri.inputStream().buffered().use { src ->
|
||||
src.mark(500)
|
||||
val magic = ByteArray(5)
|
||||
if (src.skip(257) != 257L || src.read(magic) != magic.size) {
|
||||
PushbackInputStream(uri.inputStream(), 512).use { src ->
|
||||
val head = ByteArray(512)
|
||||
if (src.read(head) != head.size) {
|
||||
console.add("! Invalid input file")
|
||||
return false
|
||||
}
|
||||
src.reset()
|
||||
src.unread(head)
|
||||
|
||||
val magic = head.copyOf(4)
|
||||
val tarMagic = Arrays.copyOfRange(head, 257, 262)
|
||||
|
||||
val alpha = "abcdefghijklmnopqrstuvwxyz"
|
||||
val alphaNum = "$alpha${alpha.uppercase(Locale.ROOT)}0123456789"
|
||||
|
@ -275,29 +403,52 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
toString()
|
||||
}
|
||||
|
||||
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
||||
srcBoot = if (tarMagic.contentEquals("ustar".toByteArray())) {
|
||||
// tar file
|
||||
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
||||
processTar(src, outFile!!.uri.outputStream())
|
||||
outStream = TarOutputStream(outFile.uri.outputStream())
|
||||
|
||||
try {
|
||||
processTar(TarInputStream(src), outStream)
|
||||
} catch (e: IOException) {
|
||||
outStream.close()
|
||||
outFile.delete()
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
// raw image
|
||||
srcBoot = installDir.getChildFile("boot.img")
|
||||
console.add("- Copying image to cache")
|
||||
src.cleanPump(srcBoot.newOutputStream())
|
||||
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
||||
outFile!!.uri.outputStream()
|
||||
outStream = outFile.uri.outputStream()
|
||||
|
||||
try {
|
||||
if (magic.contentEquals("CrAU".toByteArray())) {
|
||||
processPayload(src)
|
||||
} else if (magic.contentEquals("PK\u0003\u0004".toByteArray())) {
|
||||
processZip(ZipInputStream(src))
|
||||
} else {
|
||||
console.add("- Copying image to cache")
|
||||
installDir.getChildFile("boot.img").also {
|
||||
src.copyAndCloseOut(it.newOutputStream())
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
outStream.close()
|
||||
outFile.delete()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
if (e is NoBootException)
|
||||
console.add("! No boot image found")
|
||||
console.add("! Process error")
|
||||
outFile?.delete()
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// Patch file
|
||||
if (!patchBoot()) {
|
||||
outFile!!.delete()
|
||||
outFile.delete()
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -314,7 +465,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
}
|
||||
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||
}
|
||||
newBoot.newInputStream().cleanPump(outStream)
|
||||
newBoot.newInputStream().copyAndClose(outStream)
|
||||
newBoot.delete()
|
||||
|
||||
console.add("")
|
||||
|
@ -324,7 +475,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
console.add("****************************")
|
||||
} catch (e: IOException) {
|
||||
console.add("! Failed to output to $outFile")
|
||||
outFile!!.delete()
|
||||
outFile.delete()
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
@ -337,22 +488,6 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
}
|
||||
|
||||
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")
|
||||
if (!useRootDir) {
|
||||
// Create output files before hand
|
||||
|
@ -364,39 +499,20 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
"cd $installDir",
|
||||
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
|
||||
"KEEPVERITY=${Config.keepVerity} " +
|
||||
"PATCHVBMETAFLAG=${Config.patchVbmeta} " +
|
||||
"PATCHVBMETAFLAG=${Info.patchBootVbmeta} " +
|
||||
"RECOVERYMODE=${Config.recovery} " +
|
||||
"SYSTEM_ROOT=${Info.isSAR} " +
|
||||
"LEGACYSAR=${Info.legacySAR} " +
|
||||
"sh boot_patch.sh $srcBoot")
|
||||
val isSuccess = cmds.sh().isSuccess
|
||||
|
||||
if (!cmds.sh().isSuccess)
|
||||
return false
|
||||
shell.newJob().add("./magiskboot cleanup", "cd /").exec()
|
||||
|
||||
val job = shell.newJob().add("./magiskboot cleanup", "cd /")
|
||||
|
||||
if (isSigned) {
|
||||
console.add("- Signing boot image with verity keys")
|
||||
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
||||
try {
|
||||
val src = 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
|
||||
return isSuccess
|
||||
}
|
||||
|
||||
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
||||
|
||||
private fun postOTA(): Boolean {
|
||||
private suspend fun postOTA(): Boolean {
|
||||
try {
|
||||
val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
|
||||
context.assets.open("bootctl").writeTo(bootctl)
|
||||
|
@ -407,25 +523,27 @@ abstract class MagiskInstallImpl protected constructor(
|
|||
return false
|
||||
}
|
||||
|
||||
console.add("***************************************")
|
||||
console.add("*************************************************************")
|
||||
console.add(" Next reboot will boot to second slot!")
|
||||
console.add("***************************************")
|
||||
console.add(" Go back to System Updates and press Restart to complete OTA")
|
||||
console.add("*************************************************************")
|
||||
return true
|
||||
}
|
||||
|
||||
private fun Array<String>.eq() = shell.newJob().add(*this).to(console, logs).enqueue()
|
||||
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
|
||||
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
|
||||
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
|
||||
|
||||
protected fun 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()
|
||||
|
||||
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
|
||||
|
||||
|
@ -513,7 +631,7 @@ abstract class MagiskInstaller(
|
|||
override suspend fun exec(): Boolean {
|
||||
val success = super.exec()
|
||||
callback()
|
||||
Utils.toast(
|
||||
context.toast(
|
||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Runnable
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.AbstractExecutorService
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
|
@ -14,7 +14,9 @@ import java.security.KeyPairGenerator
|
|||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.Random
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import com.topjohnwu.magisk.core.createNewResources
|
|||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
var currentLocale: Locale = Locale.getDefault()
|
||||
|
||||
|
|
|
@ -15,9 +15,6 @@ import com.topjohnwu.magisk.core.di.AppContext
|
|||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.MessageDigest
|
||||
import kotlin.experimental.and
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
object MediaStoreUtils {
|
||||
|
@ -102,6 +99,8 @@ object MediaStoreUtils {
|
|||
|
||||
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
|
||||
|
||||
fun Uri.fileDescriptor(mode: String) = cr.openFileDescriptor(this, mode) ?: throw FileNotFoundException()
|
||||
|
||||
val Uri.displayName: String get() {
|
||||
if (scheme == "file") {
|
||||
// Simple uri wrapper over file, directly get file name
|
||||
|
@ -118,24 +117,6 @@ object MediaStoreUtils {
|
|||
return this.toString()
|
||||
}
|
||||
|
||||
fun Uri.checkSum(alg: String, reference: String) = runCatching {
|
||||
this.inputStream().use {
|
||||
val digest = MessageDigest.getInstance(alg)
|
||||
it.copyTo(object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
digest.update(b.toByte())
|
||||
}
|
||||
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
digest.update(b, off, len)
|
||||
}
|
||||
})
|
||||
val sb = StringBuilder()
|
||||
digest.digest().forEach { b -> sb.append("%02x".format(b and 0xff.toByte())) }
|
||||
sb.toString() == reference
|
||||
}
|
||||
}.getOrElse { false }
|
||||
|
||||
interface UriFile {
|
||||
val uri: Uri
|
||||
fun delete(): Boolean
|
||||
|
|
|
@ -14,14 +14,10 @@ import androidx.core.content.getSystemService
|
|||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import com.topjohnwu.magisk.ktx.registerRuntimeReceiver
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.ktx.registerRuntimeReceiver
|
||||
|
||||
typealias ConnectionCallback = (Boolean) -> Unit
|
||||
|
||||
class NetworkObserver(
|
||||
context: Context,
|
||||
private val callback: ConnectionCallback
|
||||
): DefaultLifecycleObserver {
|
||||
class NetworkObserver(context: Context): DefaultLifecycleObserver {
|
||||
private val manager = context.getSystemService<ConnectivityManager>()!!
|
||||
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
|
@ -29,11 +25,11 @@ class NetworkObserver(
|
|||
|
||||
override fun onAvailable(network: Network) {
|
||||
activeList.add(network)
|
||||
callback(true)
|
||||
postValue(true)
|
||||
}
|
||||
override fun onLost(network: Network) {
|
||||
activeList.remove(network)
|
||||
callback(!activeList.isEmpty())
|
||||
postValue(!activeList.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,7 +41,7 @@ class NetworkObserver(
|
|||
}
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (context.isIdleMode()) {
|
||||
callback(false)
|
||||
postValue(false)
|
||||
} else {
|
||||
postCurrentState()
|
||||
}
|
||||
|
@ -67,13 +63,18 @@ class NetworkObserver(
|
|||
}
|
||||
|
||||
private fun postCurrentState() {
|
||||
callback(manager.getNetworkCapabilities(manager.activeNetwork)
|
||||
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 observe(context: Context, callback: ConnectionCallback): NetworkObserver {
|
||||
return NetworkObserver(context, callback).apply { postCurrentState() }
|
||||
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
|
||||
}
|
|
@ -7,12 +7,14 @@ import com.topjohnwu.magisk.core.Config
|
|||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
||||
import com.topjohnwu.magisk.ktx.rawResource
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.core.ktx.deviceProtectedContext
|
||||
import com.topjohnwu.magisk.core.ktx.rawResource
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.util.jar.JarFile
|
||||
|
||||
|
@ -34,7 +36,9 @@ class ShellInit : Shell.Initializer() {
|
|||
val bb = jar.getJarEntry("lib/${Const.CPU_ABI}/libbusybox.so")
|
||||
localBB = context.deviceProtectedContext.cachedFile("busybox")
|
||||
localBB.delete()
|
||||
jar.getInputStream(bb).writeTo(localBB)
|
||||
runBlocking {
|
||||
jar.getInputStream(bb).writeTo(localBB, dispatcher = Dispatchers.Unconfined)
|
||||
}
|
||||
localBB.setExecutable(true)
|
||||
} else {
|
||||
localBB = File(context.applicationInfo.nativeLibraryDir, "libbusybox.so")
|
||||
|
@ -73,17 +77,17 @@ class ShellInit : Shell.Initializer() {
|
|||
fun getVar(name: String) = fastCmd("echo \$$name")
|
||||
fun getBool(name: String) = getVar(name).toBoolean()
|
||||
|
||||
Info.isSAR = getBool("SYSTEM_ROOT")
|
||||
Info.isSAR = getBool("SYSTEM_AS_ROOT")
|
||||
Info.ramdisk = getBool("RAMDISKEXIST")
|
||||
Info.vbmeta = getBool("VBMETAEXIST")
|
||||
Info.isAB = getBool("ISAB")
|
||||
Info.crypto = getVar("CRYPTOTYPE")
|
||||
Info.patchBootVbmeta = getBool("PATCHVBMETAFLAG")
|
||||
Info.legacySAR = getBool("LEGACYSAR")
|
||||
|
||||
// Default presets
|
||||
Config.recovery = getBool("RECOVERYMODE")
|
||||
Config.keepVerity = getBool("KEEPVERITY")
|
||||
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
||||
Config.patchVbmeta = getBool("PATCHVBMETAFLAG")
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.magisk.core.ktx.copyAll
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
@ -7,14 +8,14 @@ import java.util.zip.ZipEntry
|
|||
import java.util.zip.ZipInputStream
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun File.unzip(folder: File, path: String = "", junkPath: Boolean = false) {
|
||||
suspend fun File.unzip(folder: File, path: String = "", junkPath: Boolean = false) {
|
||||
inputStream().buffered().use {
|
||||
it.unzip(folder, path, junkPath)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
||||
suspend fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
||||
try {
|
||||
val zin = ZipInputStream(this)
|
||||
var entry: ZipEntry
|
||||
|
@ -34,7 +35,7 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
|||
if (!it.exists())
|
||||
it.mkdirs()
|
||||
}
|
||||
dest.outputStream().use { out -> zin.copyTo(out) }
|
||||
dest.outputStream().use { out -> zin.copyAll(out) }
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw IOException(e)
|
||||
|
|
|
@ -8,7 +8,12 @@ import android.text.Spanned
|
|||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
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.appcompat.widget.Toolbar
|
||||
import androidx.cardview.widget.CardView
|
||||
|
@ -20,7 +25,11 @@ import androidx.databinding.BindingAdapter
|
|||
import androidx.databinding.InverseBindingAdapter
|
||||
import androidx.databinding.InverseBindingListener
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.recyclerview.widget.*
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.chip.Chip
|
||||
|
|
|
@ -6,22 +6,47 @@ import androidx.databinding.ListChangeRegistry
|
|||
import androidx.databinding.ObservableList
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.AbstractList
|
||||
|
||||
open class DiffObservableList<T : DiffItem<*>>
|
||||
: AbstractList<T>(), ObservableList<T>, ListUpdateCallback {
|
||||
// Only expose the immutable List types
|
||||
interface DiffList<T : DiffItem<*>> : List<T> {
|
||||
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult
|
||||
|
||||
@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 set
|
||||
private val listeners = ListChangeRegistry()
|
||||
|
||||
override val size: Int get() = list.size
|
||||
|
||||
override fun get(index: Int) = list[index]
|
||||
|
||||
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||
override fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||
return doCalculateDiff(list, newItems)
|
||||
}
|
||||
|
||||
|
@ -48,13 +73,13 @@ open class DiffObservableList<T : DiffItem<*>>
|
|||
}
|
||||
|
||||
@MainThread
|
||||
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
||||
override fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
||||
list = ArrayList(newItems)
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
suspend fun update(newItems: List<T>) {
|
||||
override suspend fun update(newItems: List<T>) {
|
||||
val diffResult = calculateDiff(newItems)
|
||||
withContext(Dispatchers.Main) {
|
||||
update(newItems, diffResult)
|
||||
|
@ -87,3 +112,45 @@ open class DiffObservableList<T : DiffItem<*>>
|
|||
listeners.notifyRemoved(this, position, count)
|
||||
}
|
||||
}
|
||||
|
||||
private class FilterableDiffObservableList<T : DiffItem<*>>(
|
||||
private val scope: CoroutineScope
|
||||
) : DiffObservableList<T>(), FilterList<T> {
|
||||
|
||||
private var sublist: List<T> = emptyList()
|
||||
private var job: Job? = null
|
||||
private var lastFilter: ((T) -> Boolean)? = null
|
||||
|
||||
// ---
|
||||
|
||||
override fun filter(filter: (T) -> Boolean) {
|
||||
lastFilter = filter
|
||||
job?.cancel()
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
val oldList = sublist
|
||||
val newList = list.filter(filter)
|
||||
val diff = doCalculateDiff(oldList, newList)
|
||||
withContext(Dispatchers.Main) {
|
||||
sublist = newList
|
||||
diff.dispatchUpdatesTo(this@FilterableDiffObservableList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun get(index: Int): T {
|
||||
return sublist[index]
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = sublist.size
|
||||
|
||||
@MainThread
|
||||
override fun set(newItems: List<T>) {
|
||||
onRemoved(0, sublist.size)
|
||||
list = newItems
|
||||
sublist = emptyList()
|
||||
lastFilter?.let { filter(it) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
open class FilterableDiffObservableList<T : DiffItem<*>>(
|
||||
private val scope: CoroutineScope
|
||||
) : DiffObservableList<T>() {
|
||||
|
||||
private var sublist: List<T> = emptyList()
|
||||
private var job: Job? = null
|
||||
|
||||
// ---
|
||||
|
||||
fun filter(filter: (T) -> Boolean) {
|
||||
job?.cancel()
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
val oldList = sublist
|
||||
val newList = list.filter(filter)
|
||||
val diff = doCalculateDiff(oldList, newList)
|
||||
withContext(Dispatchers.Main) {
|
||||
sublist = newList
|
||||
diff.dispatchUpdatesTo(this@FilterableDiffObservableList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun get(index: Int): T {
|
||||
return sublist[index]
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = sublist.size
|
||||
}
|
|
@ -3,7 +3,7 @@ package com.topjohnwu.magisk.databinding
|
|||
import androidx.databinding.ListChangeRegistry
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ObservableList.OnListChangedCallback
|
||||
import java.util.*
|
||||
import java.util.AbstractList
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
||||
|
@ -46,11 +46,11 @@ class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
|||
return this
|
||||
}
|
||||
|
||||
fun insertList(list: ObservableList<out T>): MergeObservableList<T> {
|
||||
fun insertList(list: List<T>): MergeObservableList<T> {
|
||||
val idx = size
|
||||
lists.add(list)
|
||||
++modCount
|
||||
(list as ObservableList<T>).addOnListChangedCallback(callback)
|
||||
(list as? ObservableList<T>)?.addOnListChangedCallback(callback)
|
||||
if (list.isNotEmpty())
|
||||
listeners.notifyInserted(this, idx, list.size)
|
||||
return this
|
||||
|
@ -72,11 +72,11 @@ class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
|||
return false
|
||||
}
|
||||
|
||||
fun removeList(listToRemove: ObservableList<out T>): Boolean {
|
||||
fun removeList(listToRemove: List<T>): Boolean {
|
||||
var idx = 0
|
||||
for ((i, list) in lists.withIndex()) {
|
||||
if (listToRemove === list) {
|
||||
(list as ObservableList<T>).removeOnListChangedCallback(callback)
|
||||
(list as? ObservableList<T>)?.removeOnListChangedCallback(callback)
|
||||
lists.removeAt(i)
|
||||
++modCount
|
||||
listeners.notifyRemoved(this, idx, list.size)
|
||||
|
@ -90,8 +90,8 @@ class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
|||
override fun clear() {
|
||||
val sz = size
|
||||
for (list in lists) {
|
||||
if (list is ObservableList<*>) {
|
||||
(list as ObservableList<T>).removeOnListChangedCallback(callback)
|
||||
if (list is ObservableList) {
|
||||
list.removeOnListChangedCallback(callback)
|
||||
}
|
||||
}
|
||||
++modCount
|
||||
|
|
|
@ -4,19 +4,19 @@ import android.net.Uri
|
|||
import com.topjohnwu.magisk.MainDirections
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.events.DialogBuilder
|
||||
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class LocalModuleInstallDialog(
|
||||
private val viewModel: ModuleViewModel,
|
||||
private val uri: Uri
|
||||
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, uri.displayName))
|
||||
setMessage(context.getString(R.string.confirm_install, displayName))
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = android.R.string.ok
|
||||
onClick {
|
||||
|
|
|
@ -4,7 +4,7 @@ import com.topjohnwu.magisk.R
|
|||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
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.view.MagiskDialog
|
||||
import java.io.File
|
||||
|
@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() {
|
|||
setCancelable(true)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = R.string.install
|
||||
onClick { DownloadService.start(activity, Subject.App()) }
|
||||
onClick { DownloadEngine.startWithActivity(activity, Subject.App()) }
|
||||
}
|
||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
text = android.R.string.cancel
|
||||
|
|
|
@ -2,8 +2,7 @@ package com.topjohnwu.magisk.dialog
|
|||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.download.Action
|
||||
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.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
@ -22,9 +21,7 @@ class OnlineModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog
|
|||
dialog.apply {
|
||||
|
||||
fun download(install: Boolean) {
|
||||
val action = if (install) Action.Flash else Action.Download
|
||||
val subject = Subject.Module(item, action)
|
||||
DownloadService.start(activity, subject)
|
||||
DownloadEngine.startWithActivity(activity, Subject.Module(item, install))
|
||||
}
|
||||
|
||||
val title = context.getString(R.string.repo_install_title,
|
||||
|
|
|
@ -5,9 +5,9 @@ import android.content.Context
|
|||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.R
|
||||
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.utils.Utils
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
|
@ -38,9 +38,9 @@ class UninstallDialog : DialogBuilder {
|
|||
Shell.cmd("restore_imgs").submit { result ->
|
||||
dialog.dismiss()
|
||||
if (result.isSuccess) {
|
||||
Utils.toast(R.string.restore_done, Toast.LENGTH_SHORT)
|
||||
context.toast(R.string.restore_done, Toast.LENGTH_SHORT)
|
||||
} else {
|
||||
Utils.toast(R.string.restore_fail, Toast.LENGTH_LONG)
|
||||
context.toast(R.string.restore_fail, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
package com.topjohnwu.magisk.events
|
||||
|
||||
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: () -> Unit = {}
|
||||
private var listenerOnSuccess: () -> Unit = {}
|
||||
|
||||
init {
|
||||
builder(Builder())
|
||||
}
|
||||
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
BiometricHelper.authenticate(
|
||||
activity,
|
||||
onError = listenerOnFailure,
|
||||
onSuccess = listenerOnSuccess
|
||||
)
|
||||
}
|
||||
|
||||
inner class Builder internal constructor() {
|
||||
|
||||
fun onFailure(listener: () -> Unit) {
|
||||
listenerOnFailure = listener
|
||||
}
|
||||
|
||||
fun onSuccess(listener: () -> Unit) {
|
||||
listenerOnSuccess = listener
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -51,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(
|
||||
private val type: String,
|
||||
private val callback: ContentResultCallback
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -12,26 +12,21 @@ import androidx.core.content.pm.ShortcutManagerCompat
|
|||
import androidx.core.view.forEach
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.MainDirections
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.arch.startAnimations
|
||||
import com.topjohnwu.magisk.arch.viewModel
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
|
||||
import com.topjohnwu.magisk.ktx.startAnimations
|
||||
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
class MainViewModel : BaseViewModel()
|
||||
|
@ -63,7 +58,6 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
|||
setContentView()
|
||||
showUnsupportedMessage()
|
||||
askForHomeShortcut()
|
||||
checkStubComponent()
|
||||
|
||||
// Ask permission to post notifications for background update check
|
||||
if (Config.checkUpdate) {
|
||||
|
@ -103,7 +97,7 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
|||
// https://issuetracker.google.com/issues/124538620
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -232,22 +226,4 @@ class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
|||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun checkStubComponent() {
|
||||
if (intent.component?.className?.contains(HideAPK.PLACEHOLDER) == true) {
|
||||
// The stub APK was not properly patched, re-apply our changes
|
||||
withPermission(Manifest.permission.REQUEST_INSTALL_PACKAGES) { granted ->
|
||||
if (granted) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val apk = File(applicationInfo.sourceDir)
|
||||
HideAPK.upgrade(this@MainActivity, apk)?.let {
|
||||
startActivity(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,23 +8,30 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.JobService
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
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.utils.RootUtils
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
@SuppressLint("CustomSplashScreen")
|
||||
abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
||||
|
@ -58,7 +65,7 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||
showInvalidStateMessage()
|
||||
return@getShell
|
||||
}
|
||||
preLoad(savedInstanceState)
|
||||
initialize(savedInstanceState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +87,7 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||
onClick {
|
||||
withPermission(REQUEST_INSTALL_PACKAGES) {
|
||||
if (!it) {
|
||||
Utils.toast(R.string.install_unknown_denied, Toast.LENGTH_SHORT)
|
||||
toast(R.string.install_unknown_denied, Toast.LENGTH_SHORT)
|
||||
showInvalidStateMessage()
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
|
@ -102,7 +109,7 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||
}
|
||||
}
|
||||
|
||||
private fun preLoad(savedState: Bundle?) {
|
||||
private fun initialize(savedState: Bundle?) {
|
||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)?.let {
|
||||
// Make sure the calling package matches (prevent DoS)
|
||||
if (it == realCallingPackage)
|
||||
|
@ -112,7 +119,21 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||
}
|
||||
|
||||
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) {
|
||||
runOnUiThread {
|
||||
// Relaunch the process after package migration
|
||||
|
@ -121,6 +142,31 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
Shortcuts.setupDynamic(this)
|
||||
|
||||
|
@ -144,19 +190,4 @@ abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Bi
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRepackage(pkg: String?) {
|
||||
if (packageName != APPLICATION_ID) {
|
||||
runCatching {
|
||||
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
|
||||
packageManager.getApplicationInfo(APPLICATION_ID, 0)
|
||||
Shell.cmd("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
} else {
|
||||
if (Config.suManager.isNotEmpty())
|
||||
Config.suManager = ""
|
||||
pkg ?: return
|
||||
Shell.cmd("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,15 +4,20 @@ import android.annotation.SuppressLint
|
|||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
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.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.core.os.ProcessCompat
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
import java.util.*
|
||||
import java.util.TreeSet
|
||||
|
||||
class CmdlineListItem(line: String) {
|
||||
val packageName: String
|
||||
|
|
|
@ -11,8 +11,8 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseFragment
|
||||
import com.topjohnwu.magisk.arch.viewModel
|
||||
import com.topjohnwu.magisk.core.ktx.hideKeyboard
|
||||
import com.topjohnwu.magisk.databinding.FragmentDenyMd2Binding
|
||||
import com.topjohnwu.magisk.ktx.hideKeyboard
|
||||
import rikka.recyclerview.addEdgeSpacing
|
||||
import rikka.recyclerview.addItemSpacing
|
||||
import rikka.recyclerview.fixEdgeEffect
|
||||
|
|
|
@ -5,11 +5,11 @@ import android.view.ViewGroup
|
|||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.startAnimations
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||
import com.topjohnwu.magisk.databinding.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.ktx.startAnimations
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
|
|
@ -7,10 +7,10 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.databinding.FilterableDiffObservableList
|
||||
import com.topjohnwu.magisk.core.ktx.concurrentMap
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.filterList
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.ktx.concurrentMap
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
|
@ -23,22 +23,22 @@ class DenyListViewModel : AsyncLoadViewModel() {
|
|||
var isShowSystem = false
|
||||
set(value) {
|
||||
field = value
|
||||
query()
|
||||
doQuery(query)
|
||||
}
|
||||
|
||||
var isShowOS = false
|
||||
set(value) {
|
||||
field = value
|
||||
query()
|
||||
doQuery(query)
|
||||
}
|
||||
|
||||
var query = ""
|
||||
set(value) {
|
||||
field = value
|
||||
query()
|
||||
doQuery(value)
|
||||
}
|
||||
|
||||
val items = FilterableDiffObservableList<DenyListRvItem>(viewModelScope)
|
||||
val items = filterList<DenyListRvItem>(viewModelScope)
|
||||
val extraBindings = bindExtra {
|
||||
it.put(BR.viewModel, this)
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ class DenyListViewModel : AsyncLoadViewModel() {
|
|||
@SuppressLint("InlinedApi")
|
||||
override suspend fun doLoadWork() {
|
||||
loading = true
|
||||
withContext(Dispatchers.Default) {
|
||||
val apps = withContext(Dispatchers.Default) {
|
||||
val pm = AppContext.packageManager
|
||||
val denyList = Shell.cmd("magisk --denylist ls").exec().out
|
||||
.map { CmdlineListItem(it) }
|
||||
|
@ -63,21 +63,22 @@ class DenyListViewModel : AsyncLoadViewModel() {
|
|||
.toCollection(ArrayList(size))
|
||||
}
|
||||
apps.sort()
|
||||
items.update(apps)
|
||||
apps
|
||||
}
|
||||
query()
|
||||
items.set(apps)
|
||||
doQuery(query)
|
||||
}
|
||||
|
||||
private fun query() {
|
||||
private fun doQuery(s: String) {
|
||||
items.filter {
|
||||
fun filterSystem() = isShowSystem || !it.info.isSystemApp()
|
||||
|
||||
fun filterOS() = (isShowSystem && isShowOS) || it.info.isApp()
|
||||
|
||||
fun filterQuery(): Boolean {
|
||||
fun inName() = it.info.label.contains(query, true)
|
||||
fun inPackage() = it.info.packageName.contains(query, true)
|
||||
fun inProcesses() = it.processes.any { p -> p.process.name.contains(query, true) }
|
||||
fun inName() = it.info.label.contains(s, true)
|
||||
fun inPackage() = it.info.packageName.contains(s, true)
|
||||
fun inProcesses() = it.processes.any { p -> p.process.name.contains(s, true) }
|
||||
return inName() || inPackage() || inProcesses()
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,11 @@ import android.content.Context
|
|||
import android.content.pm.ActivityInfo
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.navigation.NavDeepLinkBuilder
|
||||
|
|
|
@ -5,23 +5,23 @@ import androidx.databinding.Bindable
|
|||
import androidx.databinding.ObservableArrayList
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.ktx.reboot
|
||||
import com.topjohnwu.magisk.core.ktx.synchronized
|
||||
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.tasks.FlashZip
|
||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.ktx.reboot
|
||||
import com.topjohnwu.magisk.ktx.synchronized
|
||||
import com.topjohnwu.magisk.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.ktx.toTime
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -34,7 +34,7 @@ class FlashViewModel : BaseViewModel() {
|
|||
|
||||
private val _state = MutableLiveData(State.FLASHING)
|
||||
val state: LiveData<State> get() = _state
|
||||
val flashing = Transformations.map(state) { it == State.FLASHING }
|
||||
val flashing = state.map { it == State.FLASHING }
|
||||
|
||||
@get:Bindable
|
||||
var showReboot = Info.isRooted
|
||||
|
@ -72,6 +72,7 @@ class FlashViewModel : BaseViewModel() {
|
|||
MagiskInstaller.Direct(outItems, logItems).exec()
|
||||
}
|
||||
Const.Value.FLASH_INACTIVE_SLOT -> {
|
||||
showReboot = false
|
||||
MagiskInstaller.SecondSlot(outItems, logItems).exec()
|
||||
}
|
||||
Const.Value.PATCH_FILE -> {
|
||||
|
|
|
@ -24,6 +24,10 @@ private interface RikkaImpl : Dev {
|
|||
override val name get() = "RikkaW"
|
||||
}
|
||||
|
||||
private interface CanyieImpl : Dev {
|
||||
override val name get() = "canyie"
|
||||
}
|
||||
|
||||
sealed class DeveloperItem : Dev {
|
||||
|
||||
abstract val items: List<IconLink>
|
||||
|
@ -61,6 +65,14 @@ sealed class DeveloperItem : Dev {
|
|||
object : IconLink.Github.User(), RikkaImpl {}
|
||||
)
|
||||
}
|
||||
|
||||
object Canyie : DeveloperItem(), CanyieImpl {
|
||||
override val items =
|
||||
listOf<IconLink>(
|
||||
object : IconLink.Twitter() { override val name = "canyie2977" },
|
||||
object : IconLink.Github.User(), CanyieImpl {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class IconLink : RvItem() {
|
||||
|
|
|
@ -14,7 +14,7 @@ import com.topjohnwu.magisk.R
|
|||
import com.topjohnwu.magisk.arch.BaseFragment
|
||||
import com.topjohnwu.magisk.arch.viewModel
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
|
||||
|
||||
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
|
||||
|
@ -25,7 +25,7 @@ class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
|
|||
override fun onStart() {
|
||||
super.onStart()
|
||||
activity?.setTitle(R.string.section_home)
|
||||
DownloadService.observeProgress(this, viewModel::onProgressUpdate)
|
||||
DownloadEngine.observeProgress(this, viewModel::onProgressUpdate)
|
||||
}
|
||||
|
||||
private fun checkTitle(text: TextView, icon: ImageView) {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package com.topjohnwu.magisk.ui.home
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
|
@ -15,6 +18,8 @@ import com.topjohnwu.magisk.core.Config
|
|||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.core.download.Subject.App
|
||||
import com.topjohnwu.magisk.core.ktx.await
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
|
@ -22,8 +27,6 @@ import com.topjohnwu.magisk.dialog.EnvFixDialog
|
|||
import com.topjohnwu.magisk.dialog.ManagerInstallDialog
|
||||
import com.topjohnwu.magisk.dialog.UninstallDialog
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.asText
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlin.math.roundToInt
|
||||
|
@ -112,7 +115,15 @@ class HomeViewModel(
|
|||
}
|
||||
|
||||
fun onLinkPressed(link: String) = object : ViewEvent(), ContextExecutor {
|
||||
override fun invoke(context: Context) = Utils.openLink(context, link.toUri())
|
||||
override fun invoke(context: Context) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, link.toUri())
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
context.toast(R.string.open_link_failed_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}.publish()
|
||||
|
||||
fun onDeletePressed() = UninstallDialog().show()
|
||||
|
|
|
@ -7,8 +7,10 @@ import android.view.MenuItem
|
|||
import android.widget.PopupMenu
|
||||
import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.ktx.reboot as systemReboot
|
||||
import com.topjohnwu.magisk.core.ktx.reboot as systemReboot
|
||||
|
||||
object RebootMenu {
|
||||
|
||||
|
@ -20,6 +22,11 @@ object RebootMenu {
|
|||
R.id.action_reboot_download -> systemReboot("download")
|
||||
R.id.action_reboot_edl -> systemReboot("edl")
|
||||
R.id.action_reboot_recovery -> systemReboot("recovery")
|
||||
R.id.action_reboot_safe_mode -> {
|
||||
val status = !item.isChecked
|
||||
item.isChecked = status
|
||||
Config.bootloop = if (status) 2 else 0
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
return true
|
||||
|
@ -29,10 +36,16 @@ object RebootMenu {
|
|||
val themeWrapper = ContextThemeWrapper(activity, R.style.Foundation_PopupMenu)
|
||||
val menu = PopupMenu(themeWrapper, activity.findViewById(R.id.action_reboot))
|
||||
activity.menuInflater.inflate(R.menu.menu_reboot, menu.menu)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||
activity.getSystemService<PowerManager>()?.isRebootingUserspaceSupported == true)
|
||||
menu.menu.findItem(R.id.action_reboot_userspace).isVisible = true
|
||||
menu.setOnMenuItemClickListener(RebootMenu::reboot)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||
activity.getSystemService<PowerManager>()?.isRebootingUserspaceSupported == true) {
|
||||
menu.menu.findItem(R.id.action_reboot_userspace).isVisible = true
|
||||
}
|
||||
if (Const.Version.isCanary()) {
|
||||
menu.menu.findItem(R.id.action_reboot_safe_mode).isChecked = Config.bootloop >= 2
|
||||
} else {
|
||||
menu.menu.findItem(R.id.action_reboot_safe_mode).isVisible = false
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ package com.topjohnwu.magisk.ui.install
|
|||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.SpannedString
|
||||
import android.widget.Toast
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.LiveData
|
||||
|
@ -19,27 +19,25 @@ import com.topjohnwu.magisk.core.Const
|
|||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.dialog.SecondSlotWarningDialog
|
||||
import com.topjohnwu.magisk.events.GetContentEvent
|
||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import io.noties.markwon.Markwon
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class InstallViewModel(
|
||||
svc: NetworkService
|
||||
) : BaseViewModel() {
|
||||
class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel() {
|
||||
|
||||
val isRooted get() = Info.isRooted
|
||||
val hideVbmeta = Info.vbmeta || Info.isSamsung || Info.isAB
|
||||
val skipOptions = Info.isEmulator || (Info.isSAR && !Info.isFDE && hideVbmeta && Info.ramdisk)
|
||||
val skipOptions = Info.isEmulator || (Info.isSAR && !Info.isFDE && Info.ramdisk)
|
||||
val noSecondSlot = !isRooted || !Info.isAB || Info.isEmulator
|
||||
|
||||
@get:Bindable
|
||||
|
@ -65,7 +63,7 @@ class InstallViewModel(
|
|||
val data: LiveData<Uri?> get() = uri
|
||||
|
||||
@get:Bindable
|
||||
var notes: Spanned = SpannableStringBuilder()
|
||||
var notes: Spanned = SpannedString("")
|
||||
set(value) = set(value, field, { field = it }, BR.notes)
|
||||
|
||||
init {
|
||||
|
@ -81,7 +79,10 @@ class InstallViewModel(
|
|||
str
|
||||
}
|
||||
}
|
||||
notes = ServiceLocator.markwon.toMarkdown(text)
|
||||
val spanned = markwon.toMarkdown(text)
|
||||
withContext(Dispatchers.Main) {
|
||||
notes = spanned
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
|
@ -103,7 +104,6 @@ class InstallViewModel(
|
|||
step,
|
||||
Config.keepVerity,
|
||||
Config.keepEnc,
|
||||
Config.patchVbmeta,
|
||||
Config.recovery
|
||||
))
|
||||
}
|
||||
|
@ -114,7 +114,6 @@ class InstallViewModel(
|
|||
step = it.step
|
||||
Config.keepVerity = it.keepVerity
|
||||
Config.keepEnc = it.keepEnc
|
||||
Config.patchVbmeta = it.patchVbmeta
|
||||
Config.recovery = it.recovery
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +121,7 @@ class InstallViewModel(
|
|||
@Parcelize
|
||||
class UriCallback : ContentResultCallback {
|
||||
override fun onActivityLaunch() {
|
||||
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
|
||||
AppContext.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
|
||||
}
|
||||
override fun onActivityResult(result: Uri) {
|
||||
uri.value = result
|
||||
|
@ -135,7 +134,6 @@ class InstallViewModel(
|
|||
val step: Int,
|
||||
val keepVerity: Boolean,
|
||||
val keepEnc: Boolean,
|
||||
val patchVbmeta: Boolean,
|
||||
val recovery: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
|
|
|
@ -8,15 +8,15 @@ import com.topjohnwu.magisk.BuildConfig
|
|||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.databinding.DiffObservableList
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.diffList
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.ktx.toTime
|
||||
import com.topjohnwu.magisk.view.TextItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -37,13 +37,13 @@ class LogViewModel(
|
|||
|
||||
// --- su log
|
||||
|
||||
val items = DiffObservableList<SuLogRvItem>()
|
||||
val items = diffList<SuLogRvItem>()
|
||||
val extraBindings = bindExtra {
|
||||
it.put(BR.viewModel, this)
|
||||
}
|
||||
|
||||
// --- magisk log
|
||||
val logs = DiffObservableList<LogRvItem>()
|
||||
val logs = diffList<LogRvItem>()
|
||||
var magiskLogRaw = " "
|
||||
|
||||
override suspend fun doLoadWork() {
|
||||
|
|
|
@ -3,18 +3,19 @@ package com.topjohnwu.magisk.ui.log
|
|||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.timeDateFormat
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import com.topjohnwu.magisk.databinding.DiffItem
|
||||
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.ktx.timeDateFormat
|
||||
import com.topjohnwu.magisk.ktx.toTime
|
||||
|
||||
class SuLogRvItem(val log: SuLog) : ObservableRvItem(), DiffItem<SuLogRvItem> {
|
||||
|
||||
override val layoutRes = R.layout.item_log_access_md2
|
||||
|
||||
val date = log.time.toTime(timeDateFormat)
|
||||
val info = genInfo()
|
||||
|
||||
@get:Bindable
|
||||
var isTop = false
|
||||
|
@ -25,4 +26,28 @@ class SuLogRvItem(val log: SuLog) : ObservableRvItem(), DiffItem<SuLogRvItem> {
|
|||
set(value) = set(value, field, { field = it }, BR.bottom)
|
||||
|
||||
override fun itemSameAs(other: SuLogRvItem) = log.appName == other.log.appName
|
||||
|
||||
private fun genInfo(): String {
|
||||
val res = AppContext.resources
|
||||
val sb = StringBuilder()
|
||||
val date = log.time.toTime(timeDateFormat)
|
||||
val toUid = res.getString(R.string.target_uid, log.toUid)
|
||||
val fromPid = res.getString(R.string.pid, log.fromPid)
|
||||
sb.append("$date\n$toUid $fromPid")
|
||||
if (log.target != -1) {
|
||||
val pid = if (log.target == 0) "magiskd" else log.target.toString()
|
||||
val target = res.getString(R.string.target_pid, pid)
|
||||
sb.append(" $target")
|
||||
}
|
||||
if (log.context.isNotEmpty()) {
|
||||
val context = res.getString(R.string.selinux_context, log.context)
|
||||
sb.append("\n$context")
|
||||
}
|
||||
if (log.gids.isNotEmpty()) {
|
||||
val gids = res.getString(R.string.supp_group, log.gids)
|
||||
sb.append("\n$gids")
|
||||
}
|
||||
sb.append("\n${log.command}")
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.view.View
|
|||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseFragment
|
||||
import com.topjohnwu.magisk.arch.viewModel
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
|
||||
import rikka.recyclerview.addEdgeSpacing
|
||||
import rikka.recyclerview.addInvalidateItemDecorationsObserver
|
||||
|
@ -21,7 +22,8 @@ class ModuleFragment : BaseFragment<FragmentModuleMd2Binding>() {
|
|||
activity?.title = resources.getString(R.string.modules)
|
||||
viewModel.data.observe(this) {
|
||||
it ?: return@observe
|
||||
viewModel.requestInstallLocalModule(it)
|
||||
val displayName = runCatching { it.displayName }.getOrNull() ?: return@observe
|
||||
viewModel.requestInstallLocalModule(it, displayName)
|
||||
viewModel.data.value = null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,10 @@ import com.topjohnwu.magisk.core.Info
|
|||
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.databinding.DiffObservableList
|
||||
import com.topjohnwu.magisk.databinding.MergeObservableList
|
||||
import com.topjohnwu.magisk.databinding.RvItem
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.diffList
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.dialog.LocalModuleInstallDialog
|
||||
import com.topjohnwu.magisk.dialog.OnlineModuleInstallDialog
|
||||
|
@ -27,7 +27,7 @@ class ModuleViewModel : AsyncLoadViewModel() {
|
|||
|
||||
val bottomBarBarrierIds = intArrayOf(R.id.module_update, R.id.module_remove)
|
||||
|
||||
private val itemsInstalled = DiffObservableList<LocalModuleRvItem>()
|
||||
private val itemsInstalled = diffList<LocalModuleRvItem>()
|
||||
|
||||
val items = MergeObservableList<RvItem>()
|
||||
val extraBindings = bindExtra {
|
||||
|
@ -84,8 +84,8 @@ class ModuleViewModel : AsyncLoadViewModel() {
|
|||
GetContentEvent("application/zip", UriCallback()).publish()
|
||||
}
|
||||
|
||||
fun requestInstallLocalModule(uri: Uri) {
|
||||
LocalModuleInstallDialog(this, uri).show()
|
||||
fun requestInstallLocalModule(uri: Uri, displayName: String) {
|
||||
LocalModuleInstallDialog(this, uri, displayName).show()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
|
|
|
@ -6,9 +6,9 @@ import android.view.View
|
|||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.ktx.activity
|
||||
import com.topjohnwu.magisk.databinding.ObservableRvItem
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.ktx.activity
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@ import com.topjohnwu.magisk.R
|
|||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.ktx.activity
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.availableLocales
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
|
@ -21,8 +21,6 @@ import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
|
|||
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
|
||||
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.ktx.activity
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.asText
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
@ -229,25 +227,14 @@ object Zygisk : BaseSettingsItem.Toggle() {
|
|||
get() = Config.zygisk
|
||||
set(value) {
|
||||
Config.zygisk = value
|
||||
DenyList.isEnabled = value
|
||||
DenyListConfig.isEnabled = value
|
||||
notifyPropertyChanged(BR.description)
|
||||
DenyList.notifyPropertyChanged(BR.description)
|
||||
}
|
||||
val mismatch get() = value != Info.isZygiskEnabled
|
||||
}
|
||||
|
||||
object DenyList : BaseSettingsItem.Toggle() {
|
||||
override val title = R.string.settings_denylist_title.asText()
|
||||
override val description get() =
|
||||
if (isEnabled) {
|
||||
if (Zygisk.mismatch)
|
||||
R.string.reboot_apply_change.asText()
|
||||
else
|
||||
R.string.settings_denylist_summary.asText()
|
||||
} else {
|
||||
R.string.settings_denylist_error.asText(R.string.zygisk.asText())
|
||||
}
|
||||
override val description get() = R.string.settings_denylist_summary.asText()
|
||||
|
||||
override var value = Config.denyList
|
||||
set(value) {
|
||||
|
@ -262,18 +249,11 @@ object DenyList : BaseSettingsItem.Toggle() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun refresh() {
|
||||
isEnabled = Zygisk.value
|
||||
}
|
||||
}
|
||||
|
||||
object DenyListConfig : BaseSettingsItem.Blank() {
|
||||
override val title = R.string.settings_denylist_config_title.asText()
|
||||
override val description = R.string.settings_denylist_config_summary.asText()
|
||||
override fun refresh() {
|
||||
isEnabled = Zygisk.value
|
||||
}
|
||||
}
|
||||
|
||||
// --- Superuser
|
||||
|
@ -284,17 +264,15 @@ object Tapjack : BaseSettingsItem.Toggle() {
|
|||
override var value by Config::suTapjack
|
||||
}
|
||||
|
||||
object Biometrics : BaseSettingsItem.Toggle() {
|
||||
override val title = R.string.settings_su_biometric_title.asText()
|
||||
override var description = R.string.settings_su_biometric_summary.asText()
|
||||
override var value by Config::suBiometric
|
||||
object Authentication : BaseSettingsItem.Toggle() {
|
||||
override val title = R.string.settings_su_auth_title.asText()
|
||||
override var description = R.string.settings_su_auth_summary.asText()
|
||||
override var value by Config::userAuth
|
||||
|
||||
override fun refresh() {
|
||||
isEnabled = BiometricHelper.isSupported
|
||||
isEnabled = Info.isDeviceSecure
|
||||
if (!isEnabled) {
|
||||
value = false
|
||||
description = R.string.no_biometric.asText()
|
||||
notifyPropertyChanged(BR.checked)
|
||||
description = R.string.settings_su_auth_insecure.asText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -357,6 +335,6 @@ object Reauthenticate : BaseSettingsItem.Toggle() {
|
|||
override var value by Config::suReAuth
|
||||
|
||||
override fun refresh() {
|
||||
isEnabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Utils.showSuperUser()
|
||||
isEnabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.O && Info.showSuperUser
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,13 +13,13 @@ import com.topjohnwu.magisk.core.Const
|
|||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.activity
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.events.AddHomeIconEvent
|
||||
import com.topjohnwu.magisk.events.BiometricEvent
|
||||
import com.topjohnwu.magisk.events.AuthEvent
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.ktx.activity
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -69,10 +69,10 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
|
|||
}
|
||||
|
||||
// Superuser
|
||||
if (Utils.showSuperUser()) {
|
||||
if (Info.showSuperUser) {
|
||||
list.addAll(listOf(
|
||||
Superuser,
|
||||
Tapjack, Biometrics, AccessMode, MultiuserMode, MountNamespaceMode,
|
||||
Tapjack, Authentication, AccessMode, MultiuserMode, MountNamespaceMode,
|
||||
AutomaticResponse, RequestTimeout, SUNotification
|
||||
))
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
|
@ -92,7 +92,7 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
|
|||
when (item) {
|
||||
DownloadPath -> withExternalRW(andThen)
|
||||
UpdateChecker -> withPostNotificationPermission(andThen)
|
||||
Biometrics -> authenticate(andThen)
|
||||
Authentication -> AuthEvent(andThen).publish()
|
||||
Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().navigate()
|
||||
DenyListConfig -> SettingsFragmentDirections.actionSettingsFragmentToDenyFragment().navigate()
|
||||
SystemlessHosts -> createHosts()
|
||||
|
@ -119,16 +119,9 @@ class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
private fun authenticate(callback: () -> Unit) {
|
||||
BiometricEvent {
|
||||
// allow the change on success
|
||||
onSuccess { callback() }
|
||||
}.publish()
|
||||
}
|
||||
|
||||
private fun createHosts() {
|
||||
Shell.cmd("add_hosts_module").submit {
|
||||
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
|
||||
AppContext.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,6 +73,8 @@ class PolicyRvItem(
|
|||
viewModel.deletePressed(this)
|
||||
}
|
||||
|
||||
override fun itemSameAs(other: PolicyRvItem) = item.uid == other.item.uid
|
||||
override fun itemSameAs(other: PolicyRvItem) = packageName == other.packageName
|
||||
|
||||
override fun contentSameAs(other: PolicyRvItem) = item.policy == other.item.policy
|
||||
|
||||
}
|
||||
|
|
|
@ -10,17 +10,21 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.databinding.*
|
||||
import com.topjohnwu.magisk.databinding.MergeObservableList
|
||||
import com.topjohnwu.magisk.databinding.RvItem
|
||||
import com.topjohnwu.magisk.databinding.bindExtra
|
||||
import com.topjohnwu.magisk.databinding.diffList
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.dialog.SuperuserRevokeDialog
|
||||
import com.topjohnwu.magisk.events.BiometricEvent
|
||||
import com.topjohnwu.magisk.events.AuthEvent
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.asText
|
||||
import com.topjohnwu.magisk.view.TextItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -34,7 +38,7 @@ class SuperuserViewModel(
|
|||
private val itemNoData = TextItem(R.string.superuser_policy_none)
|
||||
|
||||
private val itemsHelpers = ObservableArrayList<TextItem>()
|
||||
private val itemsPolicies = DiffObservableList<PolicyRvItem>()
|
||||
private val itemsPolicies = diffList<PolicyRvItem>()
|
||||
|
||||
val items = MergeObservableList<RvItem>()
|
||||
.insertList(itemsHelpers)
|
||||
|
@ -49,7 +53,7 @@ class SuperuserViewModel(
|
|||
|
||||
@SuppressLint("InlinedApi")
|
||||
override suspend fun doLoadWork() {
|
||||
if (!Utils.showSuperUser()) {
|
||||
if (!Info.showSuperUser) {
|
||||
loading = false
|
||||
return
|
||||
}
|
||||
|
@ -105,16 +109,16 @@ class SuperuserViewModel(
|
|||
fun deletePressed(item: PolicyRvItem) {
|
||||
fun updateState() = viewModelScope.launch {
|
||||
db.delete(item.item.uid)
|
||||
itemsPolicies.removeAll { it.itemSameAs(item) }
|
||||
if (itemsPolicies.isEmpty() && itemsHelpers.isEmpty()) {
|
||||
val list = ArrayList(itemsPolicies)
|
||||
list.removeAll { it.item.uid == item.item.uid }
|
||||
itemsPolicies.update(list)
|
||||
if (list.isEmpty() && itemsHelpers.isEmpty()) {
|
||||
itemsHelpers.add(itemNoData)
|
||||
}
|
||||
}
|
||||
|
||||
if (BiometricHelper.isEnabled) {
|
||||
BiometricEvent {
|
||||
onSuccess { updateState() }
|
||||
}.publish()
|
||||
if (Config.userAuth) {
|
||||
AuthEvent { updateState() }.publish()
|
||||
} else {
|
||||
SuperuserRevokeDialog(item.title) { updateState() }.show()
|
||||
}
|
||||
|
@ -153,24 +157,21 @@ class SuperuserViewModel(
|
|||
}
|
||||
|
||||
fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
|
||||
val items = itemsPolicies.filter { it.item.uid == item.item.uid }
|
||||
fun updateState() {
|
||||
viewModelScope.launch {
|
||||
val res = if (enable) R.string.su_snack_grant else R.string.su_snack_deny
|
||||
item.item.policy = if (enable) SuPolicy.ALLOW else SuPolicy.DENY
|
||||
db.update(item.item)
|
||||
itemsPolicies.forEach {
|
||||
if (it.item.uid == item.item.uid) {
|
||||
it.notifyPropertyChanged(BR.enabled)
|
||||
}
|
||||
items.forEach {
|
||||
it.notifyPropertyChanged(BR.enabled)
|
||||
}
|
||||
SnackbarEvent(res.asText(item.appName)).publish()
|
||||
}
|
||||
}
|
||||
|
||||
if (BiometricHelper.isEnabled) {
|
||||
BiometricEvent {
|
||||
onSuccess { updateState() }
|
||||
}.publish()
|
||||
if (Config.userAuth) {
|
||||
AuthEvent { updateState() }.publish()
|
||||
} else {
|
||||
updateState()
|
||||
}
|
||||
|
|
|
@ -22,17 +22,17 @@ import com.topjohnwu.magisk.arch.BaseViewModel
|
|||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.DENY
|
||||
import com.topjohnwu.magisk.core.su.SuRequestHandler
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.events.BiometricEvent
|
||||
import com.topjohnwu.magisk.events.AuthEvent
|
||||
import com.topjohnwu.magisk.events.DieEvent
|
||||
import com.topjohnwu.magisk.events.ShowUIEvent
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
|
||||
|
@ -62,7 +62,7 @@ class SuRequestViewModel(
|
|||
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
|
||||
|| event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0) {
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
Utils.toast(R.string.touch_filtered_warning, Toast.LENGTH_SHORT)
|
||||
AppContext.toast(R.string.touch_filtered_warning, Toast.LENGTH_SHORT)
|
||||
}
|
||||
return@OnTouchListener Config.suTapjack
|
||||
}
|
||||
|
@ -70,17 +70,14 @@ class SuRequestViewModel(
|
|||
}
|
||||
|
||||
private val handler = SuRequestHandler(AppContext.packageManager, policyDB)
|
||||
private lateinit var timer: CountDownTimer
|
||||
private val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
|
||||
private var timer = SuTimer(millis, 1000)
|
||||
private var initialized = false
|
||||
|
||||
fun grantPressed() {
|
||||
cancelTimer()
|
||||
if (BiometricHelper.isEnabled) {
|
||||
BiometricEvent {
|
||||
onSuccess {
|
||||
respond(ALLOW)
|
||||
}
|
||||
}.publish()
|
||||
if (Config.userAuth) {
|
||||
AuthEvent { respond(ALLOW) }.publish()
|
||||
} else {
|
||||
respond(ALLOW)
|
||||
}
|
||||
|
@ -96,7 +93,7 @@ class SuRequestViewModel(
|
|||
}
|
||||
|
||||
fun handleRequest(intent: Intent) {
|
||||
viewModelScope.launch {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
if (handler.start(intent))
|
||||
showDialog()
|
||||
else
|
||||
|
@ -125,8 +122,7 @@ class SuRequestViewModel(
|
|||
selectedItemPosition = timeoutPrefs.getInt(packageName, 0)
|
||||
|
||||
// Set timer
|
||||
val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
|
||||
timer = SuTimer(millis, 1000).apply { start() }
|
||||
timer.start()
|
||||
|
||||
// Actually show the UI
|
||||
ShowUIEvent(if (Config.suTapjack) EmptyAccessibilityDelegate else null).publish()
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
|
||||
object Utils {
|
||||
|
||||
fun toast(msg: CharSequence, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(AppContext, msg, duration).show() }
|
||||
}
|
||||
|
||||
fun toast(resId: Int, duration: Int) {
|
||||
UiThreadHandler.run { Toast.makeText(AppContext, resId, duration).show() }
|
||||
}
|
||||
|
||||
fun showSuperUser(): Boolean {
|
||||
return Info.env.isActive && (Const.USER_ID == 0
|
||||
|| Config.suMultiuserMode == Config.Value.MULTIUSER_MODE_USER)
|
||||
}
|
||||
|
||||
fun openLink(context: Context, link: Uri) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, link)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
toast(R.string.open_link_failed_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,10 +11,10 @@ import androidx.core.content.getSystemService
|
|||
import androidx.core.graphics.drawable.toIcon
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
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.ktx.getBitmap
|
||||
import com.topjohnwu.magisk.ktx.selfLaunchIntent
|
||||
import com.topjohnwu.magisk.core.ktx.getBitmap
|
||||
import com.topjohnwu.magisk.core.ktx.selfLaunchIntent
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
|
@ -67,7 +67,7 @@ object Notifications {
|
|||
|
||||
fun updateAvailable() {
|
||||
AppContext.apply {
|
||||
val intent = DownloadService.getPendingIntent(this, Subject.App())
|
||||
val intent = DownloadEngine.getPendingIntent(this, Subject.App())
|
||||
val bitmap = getBitmap(R.drawable.ic_magisk_outline)
|
||||
val builder = if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, UPDATE_CHANNEL)
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.getSystemService
|
||||
|
@ -13,8 +14,8 @@ import androidx.core.graphics.drawable.IconCompat
|
|||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.ktx.getBitmap
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.getBitmap
|
||||
|
||||
object Shortcuts {
|
||||
|
||||
|
@ -35,14 +36,28 @@ object Shortcuts {
|
|||
ShortcutManagerCompat.requestPinShortcut(context, info, null)
|
||||
}
|
||||
|
||||
private fun Context.getIconCompat(id: Int): IconCompat {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
IconCompat.createWithAdaptiveBitmap(getBitmap(id))
|
||||
else
|
||||
IconCompat.createWithBitmap(getBitmap(id))
|
||||
private fun Context.getIcon(id: Int): Icon {
|
||||
return if (isRunningAsStub) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
Icon.createWithAdaptiveBitmap(getBitmap(id))
|
||||
else
|
||||
Icon.createWithBitmap(getBitmap(id))
|
||||
} else {
|
||||
Icon.createWithResource(this, id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.getIcon(id: Int) = getIconCompat(id).toIcon(this)
|
||||
private fun Context.getIconCompat(id: Int): IconCompat {
|
||||
return if (isRunningAsStub) {
|
||||
val bitmap = getBitmap(id)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
IconCompat.createWithAdaptiveBitmap(bitmap)
|
||||
else
|
||||
IconCompat.createWithBitmap(bitmap)
|
||||
} else {
|
||||
IconCompat.createWithResource(this, id)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = 25)
|
||||
private fun getShortCuts(context: Context): List<ShortcutInfo> {
|
||||
|
@ -51,7 +66,7 @@ object Shortcuts {
|
|||
|
||||
val shortCuts = mutableListOf<ShortcutInfo>()
|
||||
|
||||
if (Utils.showSuperUser()) {
|
||||
if (Info.showSuperUser) {
|
||||
shortCuts.add(
|
||||
ShortcutInfo.Builder(context, Const.Nav.SUPERUSER)
|
||||
.setShortLabel(context.getString(R.string.superuser))
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_magisk_padded" />
|
||||
</adaptive-icon>
|
||||
<monochrome android:drawable="@drawable/ic_magisk_padded" />
|
||||
</adaptive-icon>
|
||||
|
|
|
@ -6,4 +6,9 @@
|
|||
android:drawable="@drawable/ic_extension"
|
||||
android:inset="30%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
<monochrome>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_extension"
|
||||
android:inset="30%" />
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
|
|
|
@ -6,4 +6,9 @@
|
|||
android:drawable="@drawable/ic_superuser"
|
||||
android:inset="30%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
<monochrome>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_superuser"
|
||||
android:inset="30%" />
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
|
|
|
@ -242,6 +242,14 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/l_50" />
|
||||
|
||||
<include
|
||||
item="@{DeveloperItem.Canyie.INSTANCE}"
|
||||
layout="@layout/item_developer"
|
||||
viewModel="@{viewModel}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/l_50" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
|
|
@ -108,15 +108,6 @@
|
|||
android:text="@string/keep_force_encryption"
|
||||
app:tint="?colorPrimary" />
|
||||
|
||||
<CheckBox
|
||||
style="@style/WidgetFoundation.Checkbox"
|
||||
gone="@{viewModel.hideVbmeta}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="@={Config.patchVbmeta}"
|
||||
android:text="@string/patch_vbmeta"
|
||||
app:tint="?colorPrimary" />
|
||||
|
||||
<CheckBox
|
||||
style="@style/WidgetFoundation.Checkbox"
|
||||
gone="@{Info.ramdisk}"
|
||||
|
|
|
@ -29,8 +29,13 @@
|
|||
tools:listitem="@layout/item_log_access_md2"
|
||||
tools:paddingTop="24dp" />
|
||||
|
||||
<ProgressBar
|
||||
style="@style/WidgetFoundation.ProgressBar.Indeterminate"
|
||||
goneUnless="@{viewModel.loading}"
|
||||
android:layout_marginTop="@dimen/l1" />
|
||||
|
||||
<FrameLayout
|
||||
gone="@{!viewModel.items.empty}"
|
||||
gone="@{viewModel.loading || !viewModel.items.empty}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center">
|
||||
|
|
|
@ -25,9 +25,9 @@
|
|||
|
||||
<include
|
||||
android:id="@+id/log_track_container"
|
||||
bullet="@{item.log.action ? R.drawable.ic_check_md2 : R.drawable.ic_close_md2}"
|
||||
bullet="@{item.log.action == 2 ? R.drawable.ic_check_md2 : R.drawable.ic_close_md2}"
|
||||
isBottom="@{item.isBottom}"
|
||||
isSelected="@{!item.log.action}"
|
||||
isSelected="@{item.log.action != 2}"
|
||||
isTop="@{item.isTop}"
|
||||
layout="@layout/item_log_track_md2"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -54,48 +54,22 @@
|
|||
android:text="@{item.log.appName}"
|
||||
android:textAppearance="@style/AppearanceFoundation.Body"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@+id/log_date"
|
||||
app:layout_constraintBottom_toTopOf="@+id/log_info"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/magisk" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/log_date"
|
||||
android:id="@+id/log_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.date}"
|
||||
android:text="@{item.info}"
|
||||
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
|
||||
app:layout_constraintBottom_toTopOf="@+id/log_app_details"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/log_app_name"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="06:00 PM, 10 Oct 2019" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/log_app_details"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{String.format(`%s %s`, @string/pid(item.log.fromPid), @string/target_uid(item.log.toUid))}"
|
||||
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
|
||||
app:layout_constraintBottom_toTopOf="@id/log_command"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/log_date"
|
||||
tools:text="PID: 7196 Target UID: 0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/log_command"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:text="@{item.log.command}"
|
||||
android:textAppearance="@style/AppearanceFoundation.Caption.Variant"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/log_app_details"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:text="/system/bin/sh" />
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
@ -24,11 +24,15 @@
|
|||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/module"
|
||||
style="@style/WidgetFoundation.Card"
|
||||
isEnabled="@{!item.removed && item.enabled && !item.showNotice}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="@{!item.removed && item.enabled && !item.showNotice}"
|
||||
android:focusable="@{!item.removed && item.enabled && !item.showNotice}"
|
||||
android:nextFocusRight="@id/module_indicator"
|
||||
android:onClick="@{() -> item.setEnabled(!item.enabled)}"
|
||||
app:cardBackgroundColor="@color/color_card_background_color_selector"
|
||||
tools:isEnabled="false"
|
||||
tools:layout_gravity="center"
|
||||
|
@ -95,6 +99,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/l_50"
|
||||
android:checked="@={item.enabled}"
|
||||
android:nextFocusLeft="@id/module"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/module_version_author"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1"
|
||||
|
|
|
@ -17,10 +17,13 @@
|
|||
android:layout_gravity="center">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/policy"
|
||||
style="@style/WidgetFoundation.Card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="@{item.enabled ? 1f : .5f}"
|
||||
android:focusable="true"
|
||||
android:nextFocusRight="@id/policy_indicator"
|
||||
android:onClick="@{() -> item.toggleExpand()}">
|
||||
|
||||
<LinearLayout
|
||||
|
@ -35,10 +38,10 @@
|
|||
<ImageView
|
||||
android:id="@+id/policy_app_icon"
|
||||
style="@style/WidgetFoundation.Image"
|
||||
srcCompat="@{item.icon}"
|
||||
android:layout_marginStart="@dimen/l1"
|
||||
android:layout_marginTop="@dimen/l1"
|
||||
android:layout_marginBottom="@dimen/l1"
|
||||
srcCompat="@{item.icon}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
@ -86,8 +89,9 @@
|
|||
android:id="@+id/policy_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="@={item.enabled}"
|
||||
android:layout_marginEnd="@dimen/l1"
|
||||
android:checked="@={item.enabled}"
|
||||
android:nextFocusLeft="@id/policy"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
@ -96,20 +100,20 @@
|
|||
|
||||
<LinearLayout
|
||||
android:id="@+id/policy_expand_container"
|
||||
android:orientation="horizontal"
|
||||
gone="@{!item.isExpanded}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?colorSurfaceVariant"
|
||||
android:orientation="horizontal"
|
||||
tools:visibility="visible">
|
||||
|
||||
<Button
|
||||
android:id="@+id/policy_notify"
|
||||
style="@style/WidgetFoundation.Button.Text"
|
||||
isSelected="@{item.shouldNotify}"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:minHeight="24dp"
|
||||
android:onClick="@{() -> item.toggleNotify()}"
|
||||
android:text="@string/superuser_toggle_notification"
|
||||
|
@ -131,9 +135,9 @@
|
|||
android:id="@+id/policy_log"
|
||||
style="@style/WidgetFoundation.Button.Text"
|
||||
isSelected="@{item.shouldLog}"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:minHeight="24dp"
|
||||
android:onClick="@{() -> item.toggleLog()}"
|
||||
android:text="@string/logs"
|
||||
|
@ -154,9 +158,9 @@
|
|||
<Button
|
||||
android:id="@+id/policy_delete"
|
||||
style="@style/WidgetFoundation.Button.Text"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:minHeight="24dp"
|
||||
android:onClick="@{() -> item.revoke()}"
|
||||
android:text="@string/superuser_toggle_revoke"
|
||||
|
|
|
@ -26,4 +26,9 @@
|
|||
android:id="@+id/action_reboot_edl"
|
||||
android:title="@string/reboot_edl" />
|
||||
|
||||
</menu>
|
||||
<item
|
||||
android:id="@+id/action_reboot_safe_mode"
|
||||
android:checkable="true"
|
||||
android:title="@string/reboot_safe_mode" />
|
||||
|
||||
</menu>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue