Compare commits

...

509 Commits

Author SHA1 Message Date
androidacy-user 253ccb9127 final tweaks v2
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 2e1ff92e63 Update multiple translations
Co-authored-by: Francesco Procaccini <fra.procaccc@gmail.com>
1 year ago
androidacy-user c47d9dddf4 final tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user fd8f746a22 update deps + release v2.0.2
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user eae09bf812 hide search on scroll down
fixes #313

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate b9986b30c7 Update multiple translations
Co-authored-by: PhSnake Lawyer <phsnake78@gmail.com>
1 year ago
Weblate f75da22155 Update multiple translations
Co-authored-by: Lim Xiang Yann <xiangyann@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
androidacy-user 7987aea230 fix online repo
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 3434c2202b tweak settings
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user c46b50e1ef various fixes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 4111b91fe2 improve security
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user ec9da3adf8 fix some
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user d53eeff6d4 fix path
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 6c54d19bf7 define version
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user f579aabde5 define version
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user cb277f70ca Update deps workflow
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 9e818241a1 Add deps workflow
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 6f0b8ac689 Fix actions builds
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account 5a38e9ad71
Update FUNDING.yml
Signed-off-by: Androidacy Service Account <opensource@androidacy.com>
1 year ago
Androidacy Service Account f64995055e
Add CodeQL
Signed-off-by: Androidacy Service Account <opensource@androidacy.com>
1 year ago
androidacy-user 7706527a3a Update README reflecting current changes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 67df7e89af Update multiple translations
Co-authored-by: Alex <nalexcastaneda@gmail.com>
Co-authored-by: Francesco Menghetti <t0r@gigaworld.org>
Co-authored-by: Francesco Procaccini <fra.procaccc@gmail.com>
Co-authored-by: Luis Enrique Bedoy <luisenriquebedoy1@gmail.com>
Co-authored-by: Matteo Lombardi <matteolomba@protonmail.com>
Co-authored-by: PhSnake Lawyer <phsnake78@gmail.com>
1 year ago
androidacy-user d3f1a83dbd Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user 6cf58542f7 (misc) this is why we can't have nice things
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 2b64adc3db Update multiple translations
Co-authored-by: Suu <suu@na-cat.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/ja/
Translation: FoxMMM/Fastlane
1 year ago
Fox2Code 10e11efbf2 Add javadoc to make alexandria life easier. 1 year ago
Weblate 434f338402 Update multiple translations
Co-authored-by: Sebas <sebasmetal86@tutanota.com>
1 year ago
androidacy-user 3769acaebb (fix) revert instinit
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user ec6bfd36e3 (misc) fix pb visibility
also work on fixing the update issues

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 9cf494ac13 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user c6a2963695 (misc) tweak UI
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 3333b0eb57 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Deleted User <noreply+169@weblate.org>
Co-authored-by: Sebas <sebasmetal86@tutanota.com>
Co-authored-by: reindex <ot02092000@gmail.com>
1 year ago
androidacy-user b9b4b6d0d7 (fix) pref name
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 2925b6a8c6 (fix) updates to pref handling
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 767a1fa7cd (fix) fix crash
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 9f203d6e54 (fix) fix crash
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user bfe3c698c0 (fix) small fixes again
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 128b8c51c7 (misc) test java 19
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 6d0ac44950 (misc) test java 20
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 702048e6c8 (misc) fix race condition
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 777ab751c9 (misc) flip op
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user ea1f44dfd1 (misc) enable core desugar
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 5980b20939 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user 647f53231e (misc) small fixes and updates
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 6015e8d6e8 Update multiple translations
Co-authored-by: Shreedhar Karki <elitedude.tk@gmail.com>
Co-authored-by: reindex <ot02092000@gmail.com>
1 year ago
Weblate 962e9fe248 Update multiple translations
Co-authored-by: Shreedhar Karki <elitedude.tk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Wurrtrak <wurrtrak@hotmail.com>
Co-authored-by: fujii <kandos-bafhr@yahoo.co.jp>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/sv/
Translation: FoxMMM/Fastlane
1 year ago
androidacy-user 788589c315 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user 3faf9ee1cb (fix) small fixes again
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate f442750d4b Update multiple translations
Co-authored-by: Alperen Şensoy <alp_eren.2003@hotmail.com>
Co-authored-by: Ender Zhao <ender.sq.zhao@gmail.com>
Co-authored-by: Sergio <vseryi_vr13@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
1 year ago
androidacy-user d2dc8f64ac (fix) small fixes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 6ba7ac5e90 (misc) remove redundant code
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user cbc39eadc6 (misc) whose bright idea was it to not match the lang code
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 898dcdbfa1 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user 018c6fbb22 (misc) minor tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 16f7711a37 Update multiple translations
Co-authored-by: BlackNight Shadow <blacknight5644bn@gmail.com>
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: HardcodedCat <flurduncioriel@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/pt_BR/
Translation: FoxMMM/Fastlane
1 year ago
androidacy-user 5a77f76bcb Bump version to 2.0.1
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 0aa4abca53 (chore) final bugfixes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 37c22ee35b Update multiple translations
Co-authored-by: BlackNight Shadow <blacknight5644bn@gmail.com>
Co-authored-by: Daniel Felipe <switchtegra@gmail.com>
Co-authored-by: Muhammad Rizqi Imani <rizqi.imani@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/id/
Translation: FoxMMM/Fastlane
1 year ago
androidacy-user 56f55990f7 (fix) who put that there
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user fb81d0faa4 (chore) try to fix issue with cronet
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 7e1212aac2 (chore) update lang list
istg if people don't start translating more google is gonna translate everything

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 6e95acbba4 (chore) improvements
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 231cc99cb6 (chore) code cleanup
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user f36f77214f (misc) small fix
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user f673f04f91 (misc) cleanup
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user dd9823ad5b (misc) small tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 0a6fd7316a (misc) misc version updates
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate d80e49a216 Update multiple translations
Co-authored-by: Ankit Yadav <yadavankit945@gmail.com>
Co-authored-by: mohammedelkefafy <mohammedelkefafy@gmail.com>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
Weblate c3d6954013 Update multiple translations
Co-authored-by: Ankit Yadav <yadavankit945@gmail.com>
Co-authored-by: BlackNight Shadow <blacknight5644bn@gmail.com>
Co-authored-by: Sebas <sebasmetal86@tutanota.com>
Co-authored-by: Weblate <noreply@weblate.org>
1 year ago
androidacy-user 6587d40542 (fix) temp fix
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 0b68956a86 (fix) yes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 7b4d7b579c (fix) oops
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 54b42dfa8c (misc) misc updates
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account d0ad060039
Update gradle-wrapper.properties 1 year ago
androidacy-user e6a4b6d2b5 (fix) try to address build perf issue
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 6a76a6403f (fix) try to address build perf issue
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user aaed979932 (misc) well
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 100ee6c043 Revert "(misc) gradle cache off"
This reverts commit 94edc3474d.
1 year ago
androidacy-user 94edc3474d (misc) gradle cache off
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user b71691c98c (misc) revert small change
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account 4bede75904
Enable cache for actions 1 year ago
Androidacy Service Account f824e3a915
Update settings.gradle.kts 1 year ago
Androidacy Service Account 6b8ee7c8d1
Compress libs 1 year ago
Androidacy Service Account 68dc58476c
Change ci jdk 1 year ago
Androidacy Service Account 05adafc6db
Update settings.gradle.kts 1 year ago
Androidacy Service Account 162d888684
Update build-debug.yml 1 year ago
Androidacy Service Account fa765b2dc3
Update build-debug.yml 1 year ago
Androidacy Service Account 9bcc1f6303
Add gradle enterprise 1 year ago
androidacy-user 10d2790ae8 (fix) misc fixes
- fix androidacy custom api key
- fix custom repos fully
- hopefully improve build perf

custom repos and alt repo no longer use cache due to errors in storing prop values

etc

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 9328cd8acd Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Sebas <sebasmetal86@tutanota.com>
1 year ago
androidacy-user 27fefbf3cf (fix) fix adding and removing and showing custom repos
known issues: after adding and removing one, the add button may stop showing

custom repos and alt repo no longer use cache due to errors in storing prop values

etc

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 173422e9e1 (misc) repo_repo_
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user a268013b0d (misc) work on custom repos
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user d4218e5a9d (misc) tweaks and upgrades
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 208ad83920 (fix) fix actions upload
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user a4764fddeb (fix) with every keystroke, my hope diminishes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user e04d0f3c1d (fix) fix Actions builds, this time
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user ff1bf975a2 (fix) fix Actions builds, again
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user b3e261d3d8 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user 2d201e2b64 (fix) fix settings
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 5319af4e07 Update multiple translations
Co-authored-by: Sebas <sebasmetal86@tutanota.com>
1 year ago
androidacy-user 6a706b08d5 (fix) double quote double quote
for some extremely dumb reason, gradle will generate BuildConfig.java sans double quotes for strings unless you add an extra escaped one

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 5b60ae6a58 (fix) fix Actions builds
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 8cb2ebbc7c (fix) try to fix Actions builds
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 47eff4e5c4 (migrate) migrate some minor things to kotlin
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate ba070d5d4a Update multiple translations
Co-authored-by: Sebas <sebasmetal86@tutanota.com>
1 year ago
androidacy-user 3b115044b7 (fix) misc fixes
also ensure ui consistency

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user f2d36547c9 (fix) fix custom url input validation
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user cbd40572bc (fix) bugfixes for repo enable state
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account 8a94fda8c2
Merge pull request #301 from xerta555/patch-16
Update French translation
1 year ago
androidacy-user 216d4ea4ad (misc) optimizations and fixes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Rom f7f0bdde41
Update French translation 1 year ago
Androidacy Service Account d55a75bcec
Merge pull request #300 from Fox2Code/revert-299-patch-15
Revert "Update French translation"
1 year ago
Androidacy Service Account 2bb0410264
Revert "Update French translation" 1 year ago
Androidacy Service Account 83ed8aca41
Merge pull request #299 from xerta555/patch-15
Update French translation
1 year ago
androidacy-user 36b583932f (fix) point to androidacy sentry instance
sentry saas is becoming too expensive to use, so switch back to androidacy hosted

same or stronger privacy protection, server side data scrubbing enabled, and for EU users data stays in EU

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 7effbaac2f (fix) try to fix update showing in UI
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 39bb0c119f Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Ender Zhao <ender.sq.zhao@gmail.com>
Co-authored-by: Lipcsei Sándor Gordon <lipcseisanci@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
androidacy-user af1c06985e (misc) minor UI tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 61117ed065 (misc) tweak setup
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user b0e00085a1 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user 85f41657dd (misc) minor setup tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 003d33a1c1 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Dominik Flüchter <dominik.fluechter@googlemail.com>
1 year ago
androidacy-user 63b407cb15 (misc) minor UI tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Rom c12de1dd45
Update French translation 1 year ago
androidacy-user 12a5c02669 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user ac845a0c0c (misc) tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 67167352f9 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: HardcodedCat <flurduncioriel@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/pt_BR/
Translation: FoxMMM/Fastlane
1 year ago
androidacy-user c4f1869331 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user c79f081879 (misc) UI tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 6a64aaa14f Update multiple translations
Co-authored-by: Weblate <noreply@weblate.org>
1 year ago
Weblate 31b1cb2fff Update multiple translations
Co-authored-by: HardcodedCat <flurduncioriel@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
androidacy-user 8ba2d2d98e (misc) fix settings bugs
known issue is custom repo can't be added

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user a96cee7e2e (misc) rework custom repos + tweak main layout
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 48b50a3bac Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Tino Gómez <nova6k0@gmail.com>
1 year ago
androidacy-user 633a128742 (fix) fix search for good
still need to fix custom repos and work on showing updates in new UI

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 77cd9346f0 (fix) try to fix search
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 3d92c5e0c5 (chore) add offer to translate for incomplete lang
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 6ff3b23adc (deps) bump dependencies
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user d8995d7f20 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user eb46a3a56f (chore) require webview for online functionality
if no current webview is found, foxmmm should revert to an offline only module manager - allowing install from storage and to remove modules, but no update checks or online repo functionality will work

quite a bit of the http stack now relies on webview and will crash or not work as well without it so better to avoid any issues

and besides, everything since at least 4.4 has webview built-in

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 5cbf0dbd61 Update multiple translations
Co-authored-by: Weblate <noreply@weblate.org>
1 year ago
androidacy-user acfb2caf7d some missed enc prefs
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate b000ed778f Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
1 year ago
Weblate 5daf67837c Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
androidacy-user 5ea6840cee update custom repo impl
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 99c3cd2ede switch to non-transitive r classes
and more

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 1349327a29 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user db94cf2a48 small tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate dd1e47c6de Update multiple translations
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
androidacy-user c9aab40670 misc fixes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user babc002422 use inter for readability
plus, jb mono in terminal

and some cleanup

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 10f5657e74 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
1 year ago
Weblate 92a39144b2 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Suu <suu@na-cat.com>
1 year ago
androidacy-user 0c1517460b migrate more to apache commons
no more nasty hacks, we can just use apache commons libs

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 07b67cf75b Update multiple translations
Co-authored-by: Alain Deroy <aderoy@outlook.com>
1 year ago
androidacy-user c7cc320028 misc fixes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 0ac2779bb0 finish fix for source archives
also update deps

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account 9e76087d94
Fix builds
Signed-off-by: Androidacy Service Account <opensource@androidacy.com>
1 year ago
androidacy-user 92a0d50443 rework a bunch fof stuff
work around alt repo crap (incomplete)

update workflow - fixes #296

etc etc

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 1369043f8b Update multiple translations
Co-authored-by: Alain Deroy <aderoy@outlook.com>
1 year ago
androidacy-user 1df1dc13c4 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user cb4922c2a4 hacky fix for setup view height
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate f2b3496f28 Update multiple translations
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: hunhee dong <hunhee@kakao.com>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/ko/
Translation: FoxMMM/Fastlane
1 year ago
Weblate 8cf288b25b Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Ender Zhao <ender.sq.zhao@gmail.com>
Co-authored-by: Suu <suu@na-cat.com>
Co-authored-by: Weblate <noreply@weblate.org>
1 year ago
androidacy-user 9ac56c371b rework ui a bit
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 3217eaddee rework setup
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 888b624ff6 rework http
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user c4a4ec5287 Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user cc13635f49 fix bad perf on setup init
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 66b698e6a7 Update multiple translations
Co-authored-by: Cédric M <cmouzong.2@gmail.com>
Co-authored-by: Eugênio Lloyd <me.genius@outlook.com>
1 year ago
androidacy-user dca6212925 enable debuggging for webview on debug build
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 7fdc7232cc Update multiple translations
Co-authored-by: Eugênio Lloyd <me.genius@outlook.com>
1 year ago
Weblate 92234a502c Update multiple translations
Co-authored-by: Eugênio Lloyd <me.genius@outlook.com>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/pt_BR/
Translation: FoxMMM/Fastlane
1 year ago
Weblate 945db7f167 Update multiple translations
Co-authored-by: Eugênio Lloyd <me.genius@outlook.com>
1 year ago
Weblate 4b70ee6c1d Update multiple translations
Co-authored-by: Daniel Felipe <switchtegra@gmail.com>
Co-authored-by: Eugênio Lloyd <me.genius@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: hunhee dong <hunhee@kakao.com>
Co-authored-by: nift4 <nift4@protonmail.com>
1 year ago
Androidacy Service Account 08d3ffd1f2
Merge pull request #287 from xerta555/patch-14
Update French translation
1 year ago
androidacy-user b9a62f9988 small updates
also update supported languages

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 611b8a0cbe Merge remote-tracking branch 'fox/master' 1 year ago
androidacy-user f8b95e663d various optimizations
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 245b92d6a7 Update multiple translations
Co-authored-by: Furkan Ünlütürk <unluturkfurkan@proton.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: hunhee dong <hunhee@kakao.com>
1 year ago
Weblate 82363538b4 Update multiple translations
Co-authored-by: Ignacio Cuello <cuelloignacio0@gmail.com>
1 year ago
Androidacy Service Account 00f09d0552
Merge pull request #288 from RealEthanPlayzDev/id-update-1
Update Indonesian Translation
1 year ago
RadiatedExodus a72f5389f9
Indonesian Translation - Fix line 319 missing a ">" 1 year ago
RadiatedExodus 29993f1843
Indonesian Translation - Update TL (4) 1 year ago
RadiatedExodus 2eff62f97f
Indonesian Translation - Update TL (3) 1 year ago
RadiatedExodus 3f07c7fd3f
Indonesian Translation - Update TL (2) 1 year ago
androidacy-user 71722c89b0 fix webview process handling
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
RadiatedExodus 09026470e4
Indonesian Translation - Update TL (1) 1 year ago
Rom 8fd7c35be6
Update French translation 1 year ago
Weblate 4271794823 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Lim Xiang Yann <xiangyann@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/zh_Hant/
Translation: FoxMMM/Fastlane
1 year ago
androidacy-user 8884dbeb8e fix progress bar on webview
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user d4ccb5a056 implement swipe to refresh on webview
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user dbc4797c80 rework UI a little
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user a3b3c8b547 work on some things
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account df46a7c93b
Merge pull request #285 from xerta555/patch-13
French translation update
1 year ago
androidacy-user 3bc3584715 Merge remote-tracking branch 'fox/master' 1 year ago
Weblate 344a233a94 Update multiple translations
Co-authored-by: Lim Xiang Yann <xiangyann@hotmail.com>
1 year ago
androidacy-user cf671df86b revert encryption
we'll wait for a stable jetpack security 1.1 release, this one's too slow and buggy

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 368b86984c Update multiple translations
Co-authored-by: Lim Xiang Yann <xiangyann@hotmail.com>
1 year ago
Rom 3104ca667c
French translation done 1 year ago
Androidacy Service Account b00d69cf91
Merge branch 'Fox2Code:master' into master 1 year ago
androidacy-user dac0c5ddde encrypt realms
still a lil crashy but works

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 37c158b3b9 encrypt realms
still a lil crashy but works

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate a247d801c5 Update multiple translations
Co-authored-by: CONSTANT <s.k.g@gm-top.ru>
1 year ago
Weblate a9521eb6cc Update multiple translations
Co-authored-by: Suu <suu@na-cat.com>
1 year ago
androidacy-user 5029486d4f encryption for sensitive data
everything we can reasonably encrypt is now encrypted, since the app is a rather attractive attack vector for attackers. this continues the work started in v1.1 to secure the app.

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 034753d6c4 encryption for sensitive data
everything we can reasonably encrypt is now encrypted, since the app is a rather attractive attack vector for attackers. this continues the work started in v1.1 to secure the app.

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 17cf8baee9 Update multiple translations
Co-authored-by: CONSTANT <s.k.g@gm-top.ru>
1 year ago
Rom 9e556142d0
French translation update 1 year ago
Weblate 150b9f77e0 Update multiple translations
Co-authored-by: Suu <suu@na-cat.com>
1 year ago
androidacy-user b95a842131 more bugfixes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 87de510adb Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
androidacy-user 2dd1257f37 try to fix a few bugs
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account cc12fe60fa
Merge branch 'Fox2Code:master' into master 1 year ago
androidacy-user 6d0dec6ead fix cache issues + increase performance
cache is feature complete now, custom repos adding is still having leaks

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 1f38a197de some bugfixes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account 2b89049921
Merge pull request #284 from Androidacy/master
fix cache from db
1 year ago
Androidacy Service Account e6f9fbe1cb
Merge branch 'Fox2Code:master' into master 1 year ago
androidacy-user 17ab83acd0 Fix cache usage
Some performance regressions but we'll address those eventually.

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 3bfe30290b Update multiple translations
Co-authored-by: Leo Petrović <leopetrovic11@gmail.com>
Co-authored-by: Onur Durdu <ahmetonurhandurdu@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/bs/
Translation: FoxMMM/Fastlane
1 year ago
androidacy-user f1a6c6a2bf more work on trying to fix cache
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 40b1f4cb4c fix stuff, break other stuff
aka cache is now used but that randomly makes online repo list empty :thumbs_up:

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user eb838c4147 add status notice for safe modules
other tweaks

known issues still: local module list doesnt show actions for updates etc, module cache isn't consistently used

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 565cc1660f fix update checks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 45383a53d8 improve bottom nav
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user e2661e6436 finalize bottom nav, separates module tabs properly
going to settings is still a lil janky but will work on that

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user c80833b2c0 fix updater and hook up online repo tab
fixes #223 but there's a couple outstanding bugs related to the tabs that will be fixed later

fixed lack of bg on chips in black theme

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 704771d5e2 Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
androidacy-user a8e71e1bed hook up logic for bottom nav
TODO: separate online and local modules into their own tabs

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate d27fc70cd9 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: james harvid <phanhoang1490@gmail.com>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
Weblate b630bde6c4 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Kutlay Sarımehmet <kutlaysarimehmet@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
androidacy-user fcb26c213c fix icon
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 9228c156c8 begin switch to md3 style bottom nav
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user a468d9cdd8 switch to global ns
fixes #245

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 6e7ce449c7 few fixes to UpdateActivity
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 4fc7a94f78 apply best practices
fix a lot of best practices and respect user choice for needing wifi for bg updates

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user a277a7e18e clarify add repo box
closes #216`

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 68bf2636c3 finish up in-app updater
fixes #170

once androidacy impl is finished, will switch to androidacy API over GitHub for stability and accessibility reasons

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user df41a04c15 allow update cancel
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user cb562c7aa1 finalize in-app updates
need to hook this up to update checks still

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user d763b4b85b work more on in-app updates
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 376ab671ce bg update -> auto update
it clarifies what this switch is actually for

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user ead2d3d30e fix build and chips display in some cases
closes #274 and bumps deps

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user fb11b16eaf clarify module excludes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 6dd56592ad Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
androidacy-user c17db63594 add module update exclusions
closes #272

also, updates dependencies and starts work on #170

also cleans up code some

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate d0f5d4c650 Update multiple translations
Co-authored-by: Suu <suu@na-cat.com>
1 year ago
androidacy-user d9ebb2a2c4 begin work on excluding modules from update checks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user a75e68a27c Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
androidacy-user 9e7a38ed0a encrypt cookies
only applies to okhttp requests for now, but cookies are stored and sent and on-disk they are encrypted

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 350737b4af Update multiple translations
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
Weblate b1478f2f4c Update multiple translations
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
androidacy-user 08e78d9577 rework cookies more
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 68a4c54ef8 rework cookies and improve cust repos
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 252a44f7e0 Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
androidacy-user b5389d597c [insert awesome commit message here]
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 937c7c5457 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
androidacy-user 837cd46ce3 so markdown doesnt like png
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 572731e288 various optimizations
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user c26c17e7ae tweak dataDir func
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user b95559cd9c Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
androidacy-user a54ecbc6d8 more work on realm
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 39d1604215 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Heiwa Iverol <iveroldalecu7@gmail.com>
1 year ago
Androidacy Service Account 5b3bf70f83
Merge pull request #277 from xerta555/patch-12
Update FR translation
1 year ago
androidacy-user 3e1c14f2e2 Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
androidacy-user f13ed32a22 move to Timber for logging
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Rom c3a2a145fe
Update FR translation 1 year ago
Androidacy Service Account ef0fbb1735
Merge pull request #275 from xerta555/patch-11
Update French translation
1 year ago
androidacy-user 8898d0674c huge refactoring
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 24ec7f6cc6 bump min sdk to 24
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 8619c66624 tweak crash layout
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user e551ddc0c3 various changes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 8404ebf6ec Update multiple translations
Co-authored-by: Alperen Şensoy <alp_eren.2003@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/tr/
Translation: FoxMMM/Fastlane
1 year ago
Weblate 9383b4964d Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
1 year ago
androidacy-user 21da75c3fc migrate more to using realm
it might crash but hey at least it builds right

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 2a45502d91 Update multiple translations
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: ycx <ycx1287984258@163.com>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/zh_Hans/
Translation: FoxMMM/Fastlane
1 year ago
androidacy-user f9bb004721 Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
androidacy-user 28a0e78acd bump dependencies
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 2d902a0ee1 Update multiple translations
Co-authored-by: ycx <ycx1287984258@163.com>
1 year ago
Weblate bd3eecbf5d Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Toldy Zoltan Roland <ztoldy2@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
1 year ago
Rom e9c9cf79bf
Update with default translation 1 year ago
androidacy-user cdd4092bbf more improvements
- prepare repo list db
- remove prism4j for good as it's not compatible with java 11
- various fixes

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user e3734e15d6 onto a new Realm
....realm database, anyway

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 5499ab0b43 tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 7865b62255 give up on roomdb
i have a headache now, thanks

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 606ff7d778 Revert "more fixes"
This reverts commit c92f26d65f.
1 year ago
androidacy-user 2a243e485d more fixes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account 8f095974c7
Merge branch 'Fox2Code:master' into master 1 year ago
androidacy-user c92f26d65f more fixes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Rom bb9642435d
Fix 1 year ago
Rom 89b184b5b5
Update French translation 1 year ago
Weblate ccf8bc9eb9 Update multiple translations
Co-authored-by: Muhamed <muhamed_5063@hotmail.com>
Co-authored-by: Toldy Zoltan Roland <ztoldy2@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/bs/
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/hu/
Translation: FoxMMM/Fastlane
1 year ago
Weblate 59b5f026a5 Update multiple translations
Co-authored-by: Muhamed <muhamed_5063@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
1 year ago
Weblate 8cf7cd8aa4 Update multiple translations
Co-authored-by: Weblate <noreply@weblate.org>
1 year ago
Weblate 6f78d204f7 Update multiple translations
Co-authored-by: Muhammad Fadly Saripudin <mdigreget@gmail.com>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
Weblate 45a9ef6b93 Update multiple translations
Co-authored-by: Alperen Şensoy <alp_eren.2003@hotmail.com>
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Emetto Braun <deloreandmc12@docomo.ne.jp>
Co-authored-by: Omegaplex <paulign35@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
1 year ago
androidacy-user d7f8c02302 upgrade agp and start work on db support
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Fox2Code e4fff6bb6e Minor fixes. 1 year ago
Fox2Code 0de68f3b06
Fix previous commit cause I'm dum. 1 year ago
Fox2Code 8eeabcf8f6 Thx AndroidStudio/IntellijIDEA for renaming everything for me! 1 year ago
androidacy-user c8529ee12b fail fast with no internet
trying to refresh is useless if we don't have a connection

also, updates sentry to the latest

TODO: remove Prism4j and it's dependants, it hasn't been updated since 2019

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 609a6c6b19 fixes for androidacy integration
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 3cc9d05481 Update multiple translations
Co-authored-by: Alperen Şensoy <alp_eren.2003@hotmail.com>
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: nift4 <nift4@protonmail.com>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
Weblate 9f4a3938e8 Update multiple translations
Co-authored-by: Alperen Şensoy <alp_eren.2003@hotmail.com>
Co-authored-by: nift4 <nift4@protonmail.com>
1 year ago
nift4 b5bbc8a117 fix array translation chaos 1 year ago
Nick 0c9c410f74
README: fix typo 1 year ago
androidacy-user 75763c89e9 optimizations
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user ec777e3d8e work around the renderer
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user f39b3af4fa more work
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 5d844e0911 recreate setup activity
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user f63f433104 ill show you final
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user e357c6febc Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
androidacy-user 15b257720e Rework module validation and setup
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
nift4 9dc6e6806e fixup! improve zip handler 1 year ago
androidacy-user fc3406ce08 Rework module validation and setup
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
nift4 e17e839f2d improve zip handler 1 year ago
nift4 dddbf6d1c7 drop androidTest
* unused
1 year ago
Weblate 55ec84a2de Update multiple translations
Co-authored-by: Suu <suu@na-cat.com>
1 year ago
Weblate 6c73d49aee Update multiple translations
Co-authored-by: Vlad <yobabay23@gmail.com>
1 year ago
Weblate 121baad8e7 Update multiple translations
Co-authored-by: Alperen Şensoy <alp_eren.2003@hotmail.com>
Co-authored-by: BlackNight Shadow <blacknight5644bn@gmail.com>
Co-authored-by: Suu <suu@na-cat.com>
1 year ago
Weblate 7346c4c74e Update multiple translations
Co-authored-by: Ege <efeegecoskun2009@gmail.com>
1 year ago
Androidacy Service Account fdfc7c45dd
Merge pull request #267 from ender-zhao/patch-14
Update Chinese Translation
1 year ago
Androidacy Service Account 46b4e5e097
Merge pull request #268 from ender-zhao/patch-15
Update TW translate
1 year ago
androidacy-user f75e78764d Minor tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 4209653655 Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
androidacy-user 6eac6e5e9d Update against foxcompat changes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 0ee54a2224 Start on 1.2
Note this release won't come for a bit but since I need to do some testing against foxmmm might as well

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account 487cf40bf7
Merge pull request #271 from xerta555/patch-10
Update French translation
1 year ago
Rom 624c2698f2
Improvments 1 year ago
Rom 49d0234374
Final fix i hope... 1 year ago
Rom 158cd7c582
Github don't like "&" symbol 1 year ago
Rom e8d52ecd6c
Fix again 1 year ago
Rom 93c07692ff
Merge branch 'master' into patch-10 1 year ago
Rom b17582e605
Fix Github bullshit 1 year ago
Rom 6163f20108
Translation completed 1 year ago
Weblate ea8a009207 Update multiple translations
Co-authored-by: Alperen Şensoy <alp_eren.2003@hotmail.com>
Co-authored-by: Androidacy <opensource@androidacy.com>
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
1 year ago
Rom 521e0771e3
Update French translation 1 year ago
Weblate bd726a95fd Update multiple translations
Co-authored-by: PhSnake Lawyer <phsnake78@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/ru/
Translation: FoxMMM/Fastlane
1 year ago
ender-zhao fd566365e9
Update strings.xml 1 year ago
ender-zhao a4f1677a62
Update TW translate 1 year ago
ender-zhao a1922a16f2
Update strings.xml 1 year ago
ender-zhao 92b06b7b35
Update Chinese Translation 1 year ago
androidacy-user 08989be2a3 tweak some last things
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user efd5391ef8 add zip handler to start install from a diff app
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user cb20fb4a8f correct example
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user d656d1d142 whoops
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 2573313f3a code cleanup
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 5d941ae570 Tweaks and cleanup
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 16cf6d4501 Update multiple translations
Co-authored-by: Francesco De Martino <francesco2594@gmail.com>
Co-authored-by: PhSnake Lawyer <phsnake78@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Олександр Зімін <msoftdogsua@gmail.com>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/uk/
Translation: FoxMMM/Fastlane
1 year ago
androidacy-user daf0f63689 fix ellipsis more
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user b956aaeef7 fix ellipsis
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user ad22378070 who did that
oh yeah, it was me 🤦

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 46d66f31bf Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
androidacy-user faf19b1146 rework README.md
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 4dbd821732 Update multiple translations
Co-authored-by: Francesco De Martino <francesco2594@gmail.com>
Co-authored-by: PhSnake Lawyer <phsnake78@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: nift4 <nift4@protonmail.com>
1 year ago
Weblate 5d90198e25 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
1 year ago
Weblate 8910b811cc Update multiple translations
Co-authored-by: Daniel Felipe <switchtegra@gmail.com>
Co-authored-by: PhSnake Lawyer <phsnake78@gmail.com>
1 year ago
Weblate 0324508aa2 Update multiple translations
Co-authored-by: Daniel Felipe <switchtegra@gmail.com>
Co-authored-by: PhSnake Lawyer <phsnake78@gmail.com>
Co-authored-by: nift4 <nift4@protonmail.com>
1 year ago
androidacy-user 01214cf7f2 various tweaks
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 30d77941ac Fix webview complaints
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user d675e6702a Fix some weird layout issues
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 9e9793ddcd Revert "A few tweaks"
This reverts commit 63aa102963.
1 year ago
androidacy-user c318911cfa Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
androidacy-user 858c4e7eb6 Fix typos
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 63aa102963 A few tweaks
v1.1.0 coming soon(tm)

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user d2bfab8ea7 Various fixes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate 730bb1f45b Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
1 year ago
Weblate 18090b033b Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
1 year ago
Weblate fed797c51f Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
1 year ago
androidacy-user 3ba8f4bc37 Work on setup and enhance themes
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 3c990bf8af Fixes for transparent theme and setup
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 9a79029cfc Work on setup experience
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 18d07d81b5 Rework a few things
Current known bug: disabling repo on setup hides it from SettingsActivity

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account 16731ac2c4
Merge pull request #263 from Moondarker/actions-fix
Fix GitHub Actions artifact uploads after 209f271
1 year ago
Moondarker ba8b5ee425 Fixes GitHub Actions artifact uploads after 209f271 1 year ago
androidacy-user dbf0d6d35c Work on Sentry, fix some crashing
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user e2e4b952f9 Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
Androidacy Service Account 4f9fdad91a
Merge pull request #259 from Vladi69/master
Update strings.xml
1 year ago
Vladi69 86ee7ae12b
Update strings.xml 1 year ago
androidacy-user ba1357e307 Work on SetupWizard
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user ee4ad76b43 fixes and stuff
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 589eab5124 Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 1 year ago
Fox2Code ce03a0b36a My brain is melting right now. 1 year ago
Weblate 93c4789107 Update multiple translations
Co-authored-by: Alain Deroy <aderoy@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/nl/
Translation: FoxMMM/Fastlane
1 year ago
androidacy-user a92da12849 Pretty output names
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 37b6e1659a Pretty output names
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 209f2711e3 Pretty output names
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 615f243485 Change output apk name
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user e86c10dc28 Prepare for 1.0.3 release
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account e473821ebc
Merge pull request #249 from ender-zhao/patch-12
Update Translate
1 year ago
Androidacy Service Account 7892559bb3
Merge pull request #250 from tugaia56/patch-3
Update strings.xml
1 year ago
Androidacy Service Account d153898197
Merge pull request #251 from tugaia56/patch-4
Update arrays.xml
1 year ago
Androidacy Service Account 69448b8bec
Merge pull request #252 from xerta555/patch-9
Update French translation
1 year ago
Androidacy Service Account f7a176e9d2
Merge pull request #253 from Daviteusz/patch-1
Update PL language
1 year ago
Androidacy Service Account 38e3014335
Merge pull request #254 from Nitroandaccounts/patch-2
Update README.md
1 year ago
Joshua Birger 0164a4e485
Update README.md
Fix unintentional spelling mistakes
1 year ago
androidacy-user e378f604b6 Fix themes
Material3 is now used regardless of API version. Switches should no longer be invisible for < a12

Oh, and fixed a couple crashes and yes i did pet that kitten

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Daviteusz d5e3147667
Update PL language 1 year ago
Rom e4b3968340
Update French translation 1 year ago
Tullio b5f2ccc3db
Update arrays.xml 1 year ago
Tullio c24be8b5aa
Update strings.xml 1 year ago
ender-zhao eb4f050b63
Update arrays.xml 1 year ago
ender-zhao db50325f41
Update strings.xml 1 year ago
androidacy-user 6747c11b5b Missed one
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user a5cd1daaa7 Tweaks, fixes, and kittens
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Androidacy Service Account 482a4d2b90
Merge pull request #246 from Daviteusz/patch
Added a separate artifact for each build
1 year ago
Weblate 8dcccecc78 Update multiple translations
Co-authored-by: Weblate <noreply@weblate.org>
1 year ago
Daviteusz d5fdfc9abe
Added a separate artifact for each build 1 year ago
Weblate bd7de0f474 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
1 year ago
Fox2Code e0ecd8f960 Do some fixes with my last 2 working braincells. 1 year ago
Weblate 876b7987ff Update multiple translations
Co-authored-by: Alperen Şensoy <alp_eren.2003@hotmail.com>
1 year ago
Androidacy Service Account 8ac1d3bce9
Merge pull request #242 from Androidacy/master
1.0.1 update
1 year ago
Androidacy Service Account c5cca14495
Fix tests 1 year ago
androidacy-user f34ec443a1 Tweak setup wizard again
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user a8877af824 Tweak setup wizard and fix tests
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 414caf3dbe Working setup box
Also bumps minimum android sdk to 23 (aka Marshmallow) Android 5.x just isn't popular anymore and magisk even has poor support for it.

Also warn about blur on low-end devices

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 2c675577cb [DO NOT MERGE] Add setup screen
Also the usual fixes and whatnot

Note the setup is rather broken right now as something else is overwriting the pref right away

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 20d51c5b9c Fix
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 0721742c62 No-op impl for scrolling to online section
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user a61ca71221 Fix sentry + arch specific builds
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user edea473b09 Merge remote-tracking branch 'upstream/master' 1 year ago
Androidacy Service Account b5bb905b60
Merge pull request #239 from xerta555/patch-8
Update French translation
1 year ago
Rom 022cef982b
Update French translation 1 year ago
androidacy-user 66cb0b1813 Security fixes and optimizations
- All known hosts now have a hardcoded trusted root ca, because we're not just going to trust that the user has a-ok certs installed
- Remove some unused code
- Properly fix a couple NetworkOnMainThread errors

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
Weblate d62cd4f636 Update multiple translations
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Omegaplex <paulign35@gmail.com>
Co-authored-by: PhSnake Lawyer <phsnake78@gmail.com>
1 year ago
androidacy-user 86c46de069 Misc optimizations
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 816d94aee4 Add crash dialog
Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 37b93367bf Lots of foxes and refactoring
- Move some docs to docs folder
- Refactor sentry a little
- In order to "fix" custom repos, don't let them be disabled
- Misc updates and optimizations

Signed-off-by: androidacy-user <opensource@androidacy.com>
1 year ago
androidacy-user 26f6d4e657 Remove DG repo
Users are free to add it back manually but it's been disabled for awhile now, and when users tried to add it, it would not stay enabled because the manager determined it was a built-in repo

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 74dbd66ff7 Update transparent theme warnings
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
Androidacy Service Account 926731bebc
Merge pull request #233 from Daviteusz/master
Added gradle cache | Update PL language
2 years ago
Daviteusz 1f9fafbf59
Restored "./gradlew test" 2 years ago
androidacy-user 1e45a3dd5f Update transparent theme warnings
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 76a4696423 Revert "Re-enable monet for transparent theme"
Monet colors do not play nicely. Parts of UI don't render.

This reverts commit 12f764a4a3.
2 years ago
androidacy-user 12f764a4a3 Re-enable monet for transparent theme
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user c30bc44698 Block blur on transparent theme
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user e6039666bd Tweaks for transparent theme
Looks like this theme may not work on some devices.

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 056d88955e Bump version and add new(ish) theme
We feel this is stable enough to warrant a 1.x release so here's the version bump

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
Androidacy Service Account 3c98c9b6a1
Merge branch 'Fox2Code:master' into master 2 years ago
androidacy-user 71e11600ef Lots of changes and code cleanup
- Remove binaries because I was lied to
- Stuff
- Other stuff
- Jeez why did I wait two days to commit, I can't remember anything I did

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
Weblate 19b061242f Update multiple translations
Co-authored-by: PhSnake Lawyer <phsnake78@gmail.com>
Co-authored-by: Saleh ali <Rrrfff444@gmail.com>
2 years ago
Weblate 30fbac837a Update multiple translations
Co-authored-by: Burak Tas <burak_tas09@hotmail.com>
Co-authored-by: PhSnake Lawyer <phsnake78@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/da/
Translation: FoxMMM/Fastlane
2 years ago
Daviteusz 42d0125e85
Update PL language 2 years ago
Daviteusz 558cf31421
Added gradle cache and more
Now it is possible to build debug apks from different branches.

Gradle cache was also added to speed up the build process.
2 years ago
Weblate 41d68e4d12 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
2 years ago
Androidacy Service Account a1cc1a29c9
Merge pull request #231 from ender-zhao/patch-10
Update arrays.xml
2 years ago
Androidacy Service Account 36307183a5
Merge pull request #232 from ender-zhao/patch-11
Update strings.xml
2 years ago
ender-zhao 8f10cbf122
Update strings.xml 2 years ago
ender-zhao e638efa560
Update arrays.xml 2 years ago
Androidacy Service Account 92952e112e
Merge pull request #230 from Androidacy/master
Huge changes/update
2 years ago
androidacy-user cdc4bcd51e We're gonna be nice and include default client keys
Note they're pretty heavily rate limited but #betterthannothing

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 9f8703df56 Fix tests, again
Who even messed them up, anyway? *looks away*

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 46a4bd2934 Fix tests, again
Who even messed them up, anyway? *looks away*

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user f3d31ed380 Fix empty androidacy client id and bundle cronet
Fdroid apparently won't let us attempt to use cronet from gms because something something proprietary AAAAAAAAAAAAAAAAAAAAAAAAAAA

The scream is the two hours I spent resolving the 15189759875195791 conflicts that resulted from the change.

Also, when ANDROIDACY_CLIENT_ID is empty, do not allow the user to enable Androidacy repo and instead show a message suggesting to download official builds. May have to reword for fdroid

Oh, and now no internet actually means no internet and it'll properly notify when repos fail to update.

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user dfe53576bd Fix crashing for theme switcher
Very hacky but it's better than it crashing for non-english locales

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user f07627da59 Fix tests
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 4fa978b78c Add client ID as per androidacy spec
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 0339dd7525 Proper implementation for black theme
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user ff1cb4fbb6 Updates
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 35b00cfb61 Update androidacy api integration with new reqs
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 29e3d7e58e Add option to make dark theme a black theme
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 39cfa8c52e Allow user to permanently dismiss notifications request
Also handles pre-13 devices that have blocked notifications

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 37b19f01b6 Handle notification perm properly
Plus other refactorings

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 55b2b5c040 Fix weird proguard error
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 7c934e9987 Code cleanup and minor fixes
Still need to address custom repo toggling not being saved

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user b6077f2256 Loads of work
- General refactoring
- Significant speed improvements using cronet (currently depends on gms and will fallback without)
- Fix androidacy downloads
- More i probably forgot

Androidacy tokens and esp custom ones need more work but this is a good start

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
Androidacy Service Account 05a29b9a81
Merge branch 'Fox2Code:master' into master 2 years ago
Weblate 494aab40a3 Update multiple translations
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/ro/
Translation: FoxMMM/Fastlane
2 years ago
Weblate a916f6db31 Update multiple translations
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
2 years ago
Androidacy Service Account 596776524b
Merge branch 'Fox2Code:master' into master 2 years ago
Weblate d62612c73b Update multiple translations
Co-authored-by: SHAGGYGOD <kr337909@gmail.com>
2 years ago
Weblate 9dc99479b1 Update multiple translations
Co-authored-by: SHAGGYGOD <kr337909@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/es/
Translation: FoxMMM/Fastlane
2 years ago
Weblate 6ce8a250b0 Update multiple translations
Co-authored-by: SHAGGYGOD <kr337909@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
2 years ago
Weblate 8f5852a128 Update multiple translations
Co-authored-by: FZL <rudees@outlook.com>
2 years ago
Androidacy Service Account b188ccc103
Merge pull request #222 from Daviteusz/master
Removed warnings after building debug apk
2 years ago
Daviteusz adf10122f0
Removed warnings after building debug apk 2 years ago
Androidacy Service Account c88de9cb12
Update README.md 2 years ago
Androidacy Service Account 801c136765
Update README.md 2 years ago
Androidacy Service Account f20d95e2d3
Merge branch 'Fox2Code:master' into master 2 years ago
Weblate 4024c6d661 Update multiple translations
Co-authored-by: Daviteusz <imefiu3@gmail.com>
2 years ago
Weblate 58c5b628a0 Update multiple translations
Co-authored-by: Sirichai sookphon <sirioh23@outlook.co.th>
2 years ago
Weblate 1d4e85f3af Update multiple translations
Co-authored-by: Omegaplex <paulign35@gmail.com>
Co-authored-by: Sirichai sookphon <sirioh23@outlook.co.th>
Co-authored-by: Weblate <noreply@weblate.org>
2 years ago
ender-zhao 93ca7e06e9
Update strings.xml (#220) 2 years ago
Fox2Code 09671850f7
Update README.md 2 years ago
Fox2Code 8a59f75cc5
Update README.md 2 years ago
Fox2Code dc58ee63ba
Merge pull request #218 from xerta555/patch-7
Update French translation
2 years ago
Rom 3871e387c6
Fix 2 years ago
Weblate ad83719c8d Update multiple translations
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Omegaplex <paulign35@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: nift4 <nift4@protonmail.com>
2 years ago
Rom 44eed04002
Update French translation 2 years ago
androidacy-user b6fc43783d Merge branch 'master' of https://github.com/Fox2Code/FoxMagiskModuleManager 2 years ago
Fox2Code 0b0079d91b 0.6.7 - Maintenance Update 2 years ago
Fox2Code 0f0f579d13 Allow copying links from the settings by long clicking 2 years ago
androidacy-user 154e5d715b Update with upstream
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user 2171364db8 Replace captcha webview with warning
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
Fox2Code d4ab506798 Merge remote-tracking branch 'origin/master' 2 years ago
Fox2Code 78a72ba36f Re-implement Androidacy captcha error handler 2 years ago
Fox2Code 414bd5d340
Merge pull request #215 from xerta555/patch-6
Update French translation
2 years ago
androidacy-user 8b3d26a4d4 Tweak 403 handling
Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
androidacy-user b6ce2a62f0 Fix: certificate change
Also (hopefully) better handling for user captchas

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
Rom 946847ef88
Fix 2 years ago
Rom ceac162ba3
Update French translation 2 years ago
Fox2Code e3a7420bea Use `sentry.properties` existence over build flavor to set sentry enabled state. (Fix #214)
This will also disable sentry by default on community builds/forks.
2 years ago
Weblate a082e1fbd4 Update multiple translations
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Alperen Şensoy <alp_eren.2003@hotmail.com>
Co-authored-by: Daviteusz <imefiu3@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/pl/
Translation: FoxMMM/Fastlane
2 years ago
Fox2Code a76040eb12 Fix-up "Overall optimizations" 2 years ago
Androidacy Service Account 112d94b86a
Merge pull request #213 from Androidacy/master
Overall optimizations
2 years ago
androidacy-user ff1afd0416 Overall optimizations
- Clarify some failures
- Optimize build times
- Update dependencies
- Make absolute sure sentry doesn't send PII

Signed-off-by: androidacy-user <opensource@androidacy.com>
2 years ago
Nick adc7ddb731
Rework translation section in README 2 years ago
Weblate a056a32099 Update multiple translations
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Muhammad Rizqi Imani <rizqi.imani@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/id/
Translate-URL: http://translate.nift4.org/projects/foxmmm/fastlane/nb_NO/
Translation: FoxMMM/Fastlane
2 years ago

@ -1 +1,2 @@
custom: ["https://www.paypal.com/paypalme/fox2code"]
patreon: Androidacy
custom: ["https://www.androidacy.com/membership-join/#utm_source=github&utm_medium=web&utm_campaign=ghsponsors"]

@ -1,51 +1,94 @@
#file: noinspection SpellCheckingInspection
name: Generate APK Debug
on:
# Triggers the workflow on push or pull request events but only for default and protected branches
push:
branches:
- master
- debug
- '*'
paths-ignore:
- '**.md'
pull_request:
branches:
- master
- debug
branches:
- '*'
paths-ignore:
- '**.md'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v1
- name: Check out repository
uses: actions/checkout@v3
with:
submodules: true
- name: Set up Java 19
uses: actions/setup-java@v3
with:
java-version: 19
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Set Up JDK
uses: actions/setup-java@v1
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
java-version: 11
gradle-home-cache-includes: |
caches
notifications
jdks
${{ github.workspace }}/.gradle/configuration-cache
- name: Change wrapper permissions
run: chmod +x ./gradlew
- name: Run tests
run: ./gradlew test
# temporary disabled
# - name: Run tests
# run: ./gradlew test
# Create APK Debug
- name: Build apk debug
run: ./gradlew app:assembleDefaultDebug
# will not upload, just build to check if it builds
- name: Build apk fdroid-debug
run: ./gradlew app:assembleFdroidDebug
# Upload Artifact Build
# Noted For Output [module-name]/build/outputs/apk
- name: Upload apk debug
# UPLOAD ARTIFACT SECTION
# Will be shorter, when https://github.com/actions/upload-artifact/pull/354 will be merged
# FoxMMM-default-debug
- name: Upload FoxMMM-default-arm64-v8a-debug
uses: actions/upload-artifact@v3
with:
name: FoxMMM-default-arm64-v8a-debug
path: app/build/outputs/apk/default/debug/*-default-arm64-v8a-debug.apk
- name: Upload FoxMMM-default-armeabi-v7a-debug
uses: actions/upload-artifact@v3
with:
name: FoxMMM-default-armeabi-v7a-debug
path: app/build/outputs/apk/default/debug/*-default-armeabi-v7a-debug.apk
- name: Upload FoxMMM-default-universal-debug
uses: actions/upload-artifact@v3
with:
name: FoxMMM-default-universal-debug
path: app/build/outputs/apk/default/debug/*-default-universal-debug.apk
- name: Upload FoxMMM-default-x86-debug
uses: actions/upload-artifact@v3
with:
name: FoxMmm-debug
path: app/build/outputs/apk/default/debug/app-default-debug.apk
name: FoxMMM-default-x86-debug
path: app/build/outputs/apk/default/debug/*-default-x86-debug.apk
- name: Upload apk fdroid-debug
- name: Upload FoxMMM-default-x86_64-debug
uses: actions/upload-artifact@v3
with:
name: FoxMmm-fdroid-debug
path: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk
name: FoxMMM-default-x86_64-debug
path: app/build/outputs/apk/default/debug/*-default-x86_64-debug.apk

@ -0,0 +1,94 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request: # The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '24 17 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'java' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
- name: Set up Java 19
uses: actions/setup-java@v3
with:
java-version: 19
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
gradle-home-cache-includes: |
caches
notifications
jdks
${{ github.workspace }}/.gradle/configuration-cache
- name: Change wrapper permissions
run: chmod +x ./gradlew
- name: Build apk debug
run: ./gradlew app:assembleDefaultDebug
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

@ -0,0 +1,39 @@
name: Dependencies
on:
push:
branches:
- master # run the action on your projects default branch
pull_request:
branches:
- master # run the action on your projects default branch
jobs:
build:
name: Dependencies
runs-on: ubuntu-latest
permissions: # The Dependency Submission API requires write permission
contents: write
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: Set up JDK 19
uses: actions/setup-java@v3
with:
java-version: 19
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Change wrapper permissions
run: chmod +x ./gradlew
- name: Run snapshot action
uses: mikepenz/gradle-dependency-submission@v1
with:
gradle-project-path: "."
gradle-build-module: ":app"

6
.gitignore vendored

@ -4,8 +4,14 @@
/.idea/
.DS_Store
/build
/app/build
/app/default
/app/fdroid
/app/play
/captures
.externalNativeBuild
.cxx
local.properties
sentry.properties
androidacy.properties
app/google-services.json

@ -1,85 +1,110 @@
# Fox's Magisk Module Manager
# Androidacy Module Manager
## Fox Module contest
### Developed by Androidacy. Find us on the web [here](https://www.androidacy.com/?utm_source=fox-readme&utm_medium=web&utm_campagin=github).
[NoStorageRestrict](https://github.com/Magisk-Modules-Alt-Repo/NoStorageRestrict) by [@DanGLES3](https://github.com/DanGLES3) won via vote in community telegram server.
_If you're seeing this at the Fox2Code repo, the new repo is at [Androidacy/AndroidacyModuleManager](https://github.com/Androidacy/AndroidacyModuleManager)! The old repo may not receive consistent updates anymore!_
Module description: Removes the restriction when selecting folders (Downloads/Android) through the file manager on Android 11 and higher
## About
## Join us on Telegram!
The official Magisk Manager app has dropped it's support for downloading online modules, leaving users without a way to easily search for and download them. This app was created to help users download and install modules, and manage their own modules.
[![Telegram Group](https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2FFox2Code_Chat)](https://telegram.dog/Fox2Code_Chat)
**This app is not officially supported by Magisk or its developers**
## Screenshots
**The modules shown in this app are not affiliated with this app or Magisk**.
Main activity:
[<img src="screenshot-dark.jpg" width="250"/>](screenshot-dark.jpg)
[<img src="screenshot-light.jpg" width="250"/>](screenshot-light.jpg)
## Features
- Download and install modules
- Manage your own modules
- Search for and download modules
- Supports custom repos
- Separate lists of local and remote modules
- Check for module updates automatically
- Monet theming
- Fully MD3 themed
- (Coming soon) Repo creation wizard
- (Coming soon) Module creator
## What is this?
## Community
The official Magisk has dropped support to download online modules, so I made Fox's Magisk Module Manager to help you download and install Magisk modules.
[![Telegram Group](https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fandroidacy_discussions)](https://telegram.dog/androidacy_discussions)
<a href="https://translate.nift4.org/engage/foxmmm/">
<img src="https://translate.nift4.org/widgets/foxmmm/-/foxmmm/svg-badge.svg" alt="Translation status" />
</a>
**This app is not officially supported by Magisk or its developers**
### We'd like to thank Fox2Code for his initial work on the app.
**The modules shown in this app are not affiliated with this app or Magisk**
(Please contact repo owners instead)
This app was previously known as Fox's Magisk Module Manager (FoxMMM) and may still be referred to as that. Androidacy thanks Fox2Code for their initial work on this app, and wishes him the best in his future endeavours.
## Screenshots
Main activity:
| Dark theme | Light theme |
|:-----------------------------------------------------------------------------:|:-------------------------------------------------------------------------------:|
| [<img src="docs/screenshot-dark.jpg" width="250"/>](docs/screenshot-dark.jpg) | [<img src="docs/screenshot-light.jpg" width="250"/>](docs/screenshot-light.jpg) |
## Requirements
Minimum:
- Android 5.0+
- Magisk 19.0+
- An internet connection
### Minimum / Recommended:
Recommended:
- Android 6.0+
- Magisk 21.2+
- An internet connection
- Android 7.0+ / Android 8.0+
- Magisk 19.0+ / Magisk 21.2+
- An internet connection / A stable wifi connection
Note: This app may require the use of a VPN in countries with a state wide firewall.
## For users
## Installation
To install the app go to [releases](https://github.com/Fox2Code/FoxMagiskModuleManager/releases),
and download and install the latest `.apk` on your device.
To install the app go to [our website](https://www.androidacy.com/downloads/?view=FoxMMM),
and download and install the latest `.apk` for your architecture on your device.
## Repositories Available
#### Please note that we reserve the right to add, remove, and change default repos at any time, and the inclusion of any repo does not equate endorsement or any agreement with or of said repo.
The app currently use these three repo as it's module sources, with it's benefits and drawback:
(Note: Each module repo can be disabled in the settings of the app)
(Note²: I do not own or monitor any of the repo, **download at your own risk**)
The app currently use the below repos as module sources, each with their own benefits and
drawbacks. Note that the app developers do not actively monitor any repos, and downloading or
installing from them is at the user's own risk. Default repos can be enabled or disabled in-app.
#### [https://github.com/Magisk-Modules-Alt-Repo](https://github.com/Magisk-Modules-Alt-Repo)
- Accepting new modules [here](https://github.com/Magisk-Modules-Alt-Repo/submission)
- Less restrictive than the original repo
#### [Androidacy](https://www.androidacy.com/magisk-modules-repository/?utm_source=fox-readme&utm_medium=web&utm_campagin=github)
- Accepting new
modules [here](https://www.androidacy.com/module-repository-applications/?utm_source=fox-readme&utm_medium=web&utm_campagin=github)
- Modules downloadable easily outside the app
- Rigorously reviewed and tested modules
- Officially supported by Fox's mmm
- May show ads to help cover infrastructure costs.
- [Read more](https://www.androidacycom/doing-it-alone-the-what-the-how-and-the-why/?utm_source=fox-readme&utm_medium=web&utm_campagin=github)
| [Privacy policy](https://www.androidacy.com/privacy/?utm_source=fox-readme&utm_medium=web&utm_campagin=github)
- Added features like module reviews, automatic VirusTotal scans, and more.
- Pays module developers for their work as part of the Revenue Sharing Program
Support:
**Support:**
[![GitHub issues](https://img.shields.io/github/issues/Magisk-Modules-Alt-Repo/submission)](https://github.com/Magisk-Modules-Alt-Repo/submission/issues)
&emsp; [![Telegram Group](https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fandroidacy_discussions)](https://telegram.dog/androidacy_discussions)
#### [https://www.androidacy.com/modules-repo/](https://www.androidacy.com/modules-repo/)
- Accepting new modules [here](https://www.androidacy.com/module-repository-applications/)
- Modules downloadable easily outside the app
#### [Magisk Alt Repo](https://github.com/Magisk-Modules-Alt-Repo)
- Accepting new modules [here](https://github.com/Magisk-Modules-Alt-Repo/submission)
- Less restrictive than the original repo
- May have lower quality, untested, or otherwise broken modules due to their policies or lack
thereof.
- Officially supported by Fox's mmm
- Contains ads to help cover server costs
- Disabled by default and no longer recommended. Kept as an alternative for those who want it
Support:
**Support:**
[![Telegram Group](https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fandroidacy_discussions)](https://telegram.dog/androidacy_discussions)
&emsp; [![GitHub issues](https://img.shields.io/github/issues/Magisk-Modules-Alt-Repo/submission)](https://github.com/Magisk-Modules-Alt-Repo/submission/issues)
If a module is in multiple repos, the manager will just pick the most up to date version
of the module, if a module is in multiple repos it will just use first registered repo.
**Please do not use GitHub issues for help or questions. GitHub issues are specifically for bug
reporting and general app feedback.**
Note: If you or a friend uploaded a module and it doesn't appear in your module
list you can disable the low quality filter in the app settings.
Go to the [developer documentation](DEVELOPERS.md) for more info.
If a module is in multiple repos, the manager will just pick the most up to date version of the
module, if a module is in multiple repos it will just use first registered repo.
## For developers
The manager can read new meta keys to allow modules to customize their own entry
The manager can read a few new meta keys to allow modules to customize their own entry
It also use `minApi`, `maxApi` and `minMagisk` in the `module.prop` to detect compatibility
And support the `support` and `donate` properties to allow them to add their own support links
@ -87,58 +112,74 @@ And if you want to be event fancier you can setup `config` to your own config ap
(Note: the manager use fallback values for some modules, see developer documentation for more info)
It also add new ways to control the installer ui via a new `#!` command system
It allow module developers to have a more customizable install experience
It allows module developers to craft a more customizable install experience.
If you created and uploaded a module and it doesn't appear in your module list you can disable
the low quality filter in the app settings after enabling developer mode. **Ideally, we recommend you to fix your module metadata rather than disabling that filter.**
For more information please check the [developer documentation](DEVELOPERS.md)
For more information please check the [developer documentation](docs/DEVELOPERS.md)
## For translators
## Help us make our app more accessible!
Go check here: https://translate.nift4.org/projects/foxmmm/foxmmm/
You may also want to check other project on this site to translate.
**We need your help!** The app has started lagging behind in translations, and we need your help to
catch up! As a reminder, translations are required to be at 60% or more to be included in the app.
Or you can try to do a pull request on GitHub:
### Weblate (recommended)
See [`app/src/main/res/values/strings.xml`](https://github.com/Fox2Code/FoxMagiskModuleManager/blob/master/app/src/main/res/values/strings.xml)
and [`app/src/main/res/values/arrays.xml`](https://github.com/Fox2Code/FoxMagiskModuleManager/blob/master/app/src/main/res/values/arrays.xml)
We use Weblate for
translations: [https://translate.nift4.org/engage/foxmmm/](https://translate.nift4.org/engage/foxmmm/)
- You can create an account and start translating
- You may need to check your spam folder for the confirmation email
### GitHub method (advanced users)
See [`app/src/main/res/values/strings.xml`](https://github.com/Androidacy/MagiskModuleManager/blob/master/app/src/main/res/values/strings.xml)
If your language is right to left don't forget to set `lang_support_rtl` to `true`.
Translators are not expected to have any previous coding experience.
## I want to add my own repo
To add you own repo to Fox's mmm it need to follow theses conditions:
- The module repo or at least one of it's owners must be known.
- Modules in the repo must be monitored, and malicious modules must be removed.
- Module repo must have a valid, working, automatically or frequently updated `modules.json`
([Example](https://github.com/Magisk-Modules-Alt-Repo/json/blob/main/modules.json))
In addition of these initial condition the repo must follow these rules:
- Repos must process and take-down off their repo module where it's removal was requested
by their original author, even if their licences legally allow their distributions.
- Repos may collect and store "mixed anonymous data" without user permission
(Anonymous means no personal data, usernames, email, or IP addresses)
(Mixed means users data must be split and not that separate data is not linkable together)
- Temporary storage of IPs address without user consent is allowed for rate limiting, GeoIP,
security reason, and must not be used for any other purpose without user explicit consent.
(GeoIP is the process of getting the country of an IP address)
- Repos may not collect and/or distribute any personal data without informing users that they do so and offering a way to opt out
- Modules owners must be aware that their modules are being hosted on the repository
(This rule doesn't apply for modules from `Magisk-Modules-Repo` last updated before 2022)
- Modules owners must be aware of any change made of the distributed version of their modules.
Please note Androidacy has their Module Repository Policies outlined [on their website](https://www.androidacy.com/module-requirements/?utm_source=foxmmm-readme&utm_medium=web). Please refer to that document for the latest changes regarding their Repository.
If all of these conditions are met you can open an issue for review.
(And don't forget to include a link to the `modules.json`)
If an existing repo is not respecting theses rules please open an issue.
If a repo is repeatedly violating these rule will be removed from the app.
Last update of theses rules are: 4 May 2022
Please note that these rules does not apply retroactively.
If your post an issue about rules violation they must violate both the version of
the rules at the moment of the incident and the latest version of the rules.
(This paragraph doesn't apply for license violation, legal requests, or illegal behaviour.)
In addition, we advise you to contact the repo host beforehand to attempt to resolve any issues. This helps avoid unnecessary conflict, and most of the time will get your issue solved quickly!
Translators do not need to have any previous coding experience.
## Add your own repos
See [the documentation](docs/add-repo.md).
## Issues with a repo
If you have a problem with a repo, please contact the repo owner **first**. If you are unable to
reach them or they are not willing to help, you can contact us as a last resort.
_The developers of this app are unable to help with any issues arising from use or installation of
modules, and may be of limited help with issues arising from a specific repo._
Default repo owners:
- Androidacy: [Telegram](https://telegram.dog/androidacy_discussions)
- Magisk-Modules-Alt-Repo: [GitHub](https://github.com/Magisk-Modules-Alt-Repo/submission/issues)
Custom repos may have their own support channels, and we are unable to provide any support
whatsoever for them, outside of direct implementation bugs.
## License
Fox's Magisk Module Manager, the icon, and names are copyright
2021-present [Fox2Code](https://github.com/Fox2Code). The Androidacy name(s), logo, integration, and
later portions of the code are copyright
2022-present [Androidacy](https://www.androidacy.com/?utm_source=fox-repo&utm_medium=web). See
[LICENSE](LICENCE) for details. Library licenses can be found in the licenses section of the app.
Modules and their files, names, and other assets are not covered by this license or other
agreements, and are not warranted, checked, or otherwise guaranteed by the authors of this app, and may have their own licenses, agreements, and terms, of which the author(s) of this app do not check or have any responsibility for.
Some third party backend services may have additional terms, please check their
terms of service before
using them.
## EULA
The EULA can be found [here](https://www.androidacy.com/foxmmm-eula/). By downloading, installing,
or using this app you agree to the supplemental terms of the EULA.
**IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR AN CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**

@ -1,203 +0,0 @@
plugins {
id "io.sentry.android.gradle" version "3.1.5"
id 'com.android.application'
id 'com.mikepenz.aboutlibraries.plugin'
}
android {
namespace "com.fox2code.mmm"
compileSdk 33
defaultConfig {
applicationId "com.fox2code.mmm"
minSdk 21
targetSdk 33
versionCode 58
versionName "0.6.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
applicationIdSuffix '.debug'
debuggable true
// ONLY FOR TESTING SENTRY
// minifyEnabled true
// shrinkResources true
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
flavorDimensions "type"
productFlavors {
"default" {
dimension "type"
buildConfigField "boolean", "ENABLE_AUTO_UPDATER", "true"
buildConfigField "boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "true"
buildConfigField("java.util.List<String>",
"ENABLED_REPOS",
"java.util.Arrays.asList(\"magisk_alt_repo\", \"androidacy_repo\")",)
}
fdroid {
dimension "type"
applicationIdSuffix ".fdroid"
// Need to disable auto-updater for F-Droid flavor because their inclusion policy
// forbids downloading blobs from third-party websites (and F-Droid APK isn't signed
// with our keys, so the APK wouldn't install anyways).
buildConfigField "boolean", "ENABLE_AUTO_UPDATER", "false"
// Disable crash reporting for F-Droid flavor by default
buildConfigField "boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "false"
// Repo with ads or tracking feature are disabled by default for the
// F-Droid flavor.
buildConfigField("java.util.List<String>",
"ENABLED_REPOS",
"java.util.Arrays.asList(\"magisk_alt_repo\")",)
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lint {
disable 'MissingTranslation'
disable 'TypographyEllipsis'
}
}
aboutLibraries {
additionalLicenses = ["LGPL_3_0_only"]
}
sentry {
// Disable sentry on F-Droid flavor
ignoredFlavors = [ "fdroid" ]
// Disables or enables the handling of Proguard mapping for Sentry.
// If enabled the plugin will generate a UUID and will take care of
// uploading the mapping to Sentry. If disabled, all the logic
// related to proguard mapping will be excluded.
// Default is enabled.
includeProguardMapping = true
// Whether the plugin should attempt to auto-upload the mapping file to Sentry or not.
// If disabled the plugin will run a dry-run and just generate a UUID.
// The mapping file has to be uploaded manually via sentry-cli in this case.
// Default is enabled.
autoUploadProguardMapping = hasSentryConfig
// Experimental flag to turn on support for GuardSquare's tools integration (Dexguard and External Proguard).
// If enabled, the plugin will try to consume and upload the mapping file produced by Dexguard and External Proguard.
// Default is disabled.
experimentalGuardsquareSupport = true
// Disables or enables the automatic configuration of Native Symbols
// for Sentry. This executes sentry-cli automatically so
// you don't need to do it manually.
// Default is disabled.
uploadNativeSymbols = hasSentryConfig
// Does or doesn't include the source code of native code for Sentry.
// This executes sentry-cli with the --include-sources param. automatically so
// you don't need to do it manually.
// Default is disabled.
includeNativeSources = true
// Enable or disable the tracing instrumentation.
// Does auto instrumentation for specified features through bytecode manipulation.
// Default is enabled.
tracingInstrumentation {
enabled = false
}
// Enable auto-installation of Sentry components (sentry-android SDK and okhttp, timber and fragment integrations).
// Default is enabled.
// Only available v3.1.0 and above.
autoInstallation {
enabled = true
// Specifies a version of the sentry-android SDK and fragment, timber and okhttp integrations.
//
// This is also useful, when you have the sentry-android SDK already included into a transitive dependency/module and want to
// align integration versions with it (if it's a direct dependency, the version will be inferred).
//
// NOTE: if you have a higher version of the sentry-android SDK or integrations on the classpath, this setting will have no effect
// as Gradle will resolve it to the latest version.
//
// Defaults to the latest published sentry version.
sentryVersion = '6.4.1'
}
}
configurations {
implementation.exclude group: 'org.jetbrains', module: 'annotations'
}
dependencies {
// UI
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.emoji2:emoji2:1.2.0'
implementation 'androidx.emoji2:emoji2-views-helper:1.2.0'
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.webkit:webkit:1.5.0'
implementation 'com.google.android.material:material:1.6.1'
implementation "com.mikepenz:aboutlibraries:${latestAboutLibsRelease}"
implementation "dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0"
implementation "dev.rikka.rikkax.insets:insets:1.3.0"
implementation 'com.github.Dimezis:BlurView:version-2.0.2'
implementation 'com.github.KieronQuinn:MonetCompat:0.4.1'
implementation 'com.github.Fox2Code:FoxCompat:0.1.5'
// Utils
implementation 'androidx.work:work-runtime:2.7.1'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
implementation 'com.squareup.okhttp3:okhttp-brotli:4.9.3'
implementation 'com.github.topjohnwu.libsu:io:5.0.1'
implementation 'com.github.Fox2Code:RosettaX:1.0.9'
implementation 'com.github.Fox2Code:AndroidANSI:1.0.1'
// Error reporting
defaultImplementation 'io.sentry:sentry-android:6.4.1'
defaultImplementation 'io.sentry:sentry-android-fragment:6.4.1'
defaultImplementation 'io.sentry:sentry-android-okhttp:6.4.1'
defaultImplementation 'io.sentry:sentry-android-core:6.4.1'
defaultImplementation 'io.sentry:sentry-android-ndk:6.4.1'
// Markdown
implementation "io.noties.markwon:core:4.6.2"
implementation "io.noties.markwon:html:4.6.2"
implementation "io.noties.markwon:image:4.6.2"
implementation "io.noties.markwon:syntax-highlight:4.6.2"
annotationProcessor "io.noties:prism4j-bundler:2.0.0"
implementation "com.caverock:androidsvg:1.4"
// Test
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
if (hasSentryConfig) {
Properties properties = new Properties();
try (FileInputStream fis = new FileInputStream(sentryConfigFile)) {
properties.load(fis)
}
tasks.withType(Exec) {
environment "SENTRY_PROJECT", properties.getProperty("defaults.project")
environment "SENTRY_ORG", properties.getProperty("defaults.org")
environment "SENTRY_URL", properties.getProperty("defaults.url")
environment "SENTRY_AUTH_TOKEN", properties.getProperty("auth.token")
}
}

@ -0,0 +1,497 @@
@file:Suppress("UnstableApiUsage", "SpellCheckingInspection")
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
import io.sentry.android.gradle.extensions.InstrumentationFeature
import io.sentry.android.gradle.instrumentation.logcat.LogcatLevel
import java.util.Properties
plugins {
// Gradle doesn't allow conditionally enabling/disabling plugins
id("io.sentry.android.gradle")
id("com.android.application")
id("com.mikepenz.aboutlibraries.plugin")
kotlin("android")
kotlin("kapt")
}
// apply realm-android
apply(plugin = "realm-android")
val hasSentryConfig = File(rootProject.projectDir, "sentry.properties").exists()
android {
// functions to get git info: gitCommitHash, gitBranch, gitRemote
val gitCommitHash = providers.exec {
commandLine("git", "rev-parse", "--short", "HEAD")
}.standardOutput.asText.get().toString().trim()
val gitBranch = providers.exec {
commandLine("git", "rev-parse", "--abbrev-ref", "HEAD")
}.standardOutput.asText.get().toString().trim()
val gitRemote = providers.exec {
commandLine("git", "config", "--get", "remote.origin.url")
}.standardOutput.asText.get().toString().trim()
val timestamp = System.currentTimeMillis()
namespace = "com.fox2code.mmm"
compileSdk = 33
ndkVersion = "25.2.9519653"
defaultConfig {
applicationId = "com.fox2code.mmm"
minSdk = 24
targetSdk = 33
versionCode = 70
versionName = "2.0.2"
vectorDrawables {
useSupportLibrary = true
}
multiDexEnabled = true
resourceConfigurations.addAll(listOf("ar", "bs", "cs", "de", "es-rMX", "fr", "hu", "id", "ja", "nl", "pl", "pt", "pt-rBR", "ro", "ru", "tr", "uk", "zh", "zh-rTW", "en"))
}
splits {
// Configures multiple APKs based on ABI.
abi {
// Enables building multiple APKs per ABI.
isEnable = true
// By default all ABIs are included, so use reset()
// Resets the list of ABIs for Gradle to create APKs for to none.
reset()
// Specifies a list of ABIs for Gradle to create APKs for.
include("x86", "x86_64", "arm64-v8a", "armeabi-v7a")
// Specifies that you don't want to also generate a universal APK that includes all ABIs.
isUniversalApk = true
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
renderscriptOptimLevel = 3
}
getByName("debug") {
applicationIdSuffix = ".debug"
isDebuggable = true
versionNameSuffix = "-debug"
isJniDebuggable = true
isRenderscriptDebuggable = true
// ONLY FOR TESTING SENTRY
// minifyEnabled true
// shrinkResources true
// proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"),"proguard-rules.pro"
}
}
flavorDimensions.add("type")
productFlavors {
create("default") {
dimension = "type"
// current timestamp of build
buildConfigField("long", "BUILD_TIME", "$timestamp")
// debug http requests. do not set this to true if you care about performance!!!!!
buildConfigField("boolean", "DEBUG_HTTP", "false")
// Latest commit hash as BuildConfig.COMMIT_HASH
buildConfigField("String", "COMMIT_HASH", "\"$gitCommitHash\"")
// Get the current branch name as BuildConfig.BRANCH_NAME
buildConfigField("String", "BRANCH_NAME", "\"$gitBranch\"")
// Get remote url as BuildConfig.REMOTE_URL
buildConfigField("String", "REMOTE_URL", "\"$gitRemote\"")
buildConfigField("boolean", "ENABLE_AUTO_UPDATER", "true")
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "true")
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING_PII", "true")
buildConfigField("boolean", "DEFAULT_ENABLE_ANALYTICS", "true")
val properties = Properties()
if (project.rootProject.file("local.properties").exists()) {
properties.load(project.rootProject.file("local.properties").reader())
// grab matomo.url
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"" + properties.getProperty(
"matomo.url", "https://s-api.androidacy.com/matomo.php"
) + "\""
)
} else {
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"https://s-api.androidacy.com/matomo.php\""
)
}
buildConfigField("boolean", "ENABLE_PROTECTION", "true")
// Get the androidacy client ID from the androidacy.properties
val propertiesA = Properties()
// If androidacy.properties doesn"t exist, use the default client ID which is heavily
// rate limited to 30 requests per minute
if (project.rootProject.file("androidacy.properties").exists()) {
propertiesA.load(project.rootProject.file("androidacy.properties").reader())
properties.setProperty(
"client_id", "\"" + propertiesA.getProperty(
"client_id",
"5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"
) + "\""
)
} else {
properties.setProperty(
"client_id", "5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"
)
}
buildConfigField(
"String", "ANDROIDACY_CLIENT_ID", "\"" + propertiesA.getProperty("client_id") + "\""
)
buildConfigField(
"java.util.List<String>",
"ENABLED_REPOS",
"java.util.Arrays.asList(\"androidacy_repo\")",
)
}
// play variant. pretty similiar to default, but with an empty inital online repo list, and use play_client_id instead of client_id
create("play") {
dimension = "type"
applicationIdSuffix = ".play"
// current timestamp of build
buildConfigField("long", "BUILD_TIME", "$timestamp")
// debug http requests. do not set this to true if you care about performance!!!!!
buildConfigField("boolean", "DEBUG_HTTP", "false")
// Latest commit hash as BuildConfig.COMMIT_HASH
buildConfigField("String", "COMMIT_HASH", "\"$gitCommitHash\"")
// Get the current branch name as BuildConfig.BRANCH_NAME
buildConfigField("String", "BRANCH_NAME", "\"$gitBranch\"")
// Get remote url as BuildConfig.REMOTE_URL
buildConfigField("String", "REMOTE_URL", "\"$gitRemote\"")
buildConfigField("boolean", "ENABLE_AUTO_UPDATER", "false")
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "true")
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING_PII", "true")
buildConfigField("boolean", "DEFAULT_ENABLE_ANALYTICS", "true")
val properties = Properties()
if (project.rootProject.file("local.properties").exists()) {
properties.load(project.rootProject.file("local.properties").reader())
// grab matomo.url
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"" + properties.getProperty(
"matomo.url", "https://s-api.androidacy.com/matomo.php"
) + "\""
)
} else {
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"https://s-api.androidacy.com/matomo.php\""
)
}
buildConfigField("boolean", "ENABLE_PROTECTION", "true")
// Get the androidacy client ID from the androidacy.properties
val propertiesA = Properties()
// If androidacy.properties doesn"t exist, use the default client ID which is heavily
// rate limited to 30 requests per minute
if (project.rootProject.file("androidacy.properties").exists()) {
propertiesA.load(project.rootProject.file("androidacy.properties").reader())
properties.setProperty(
"client_id", "\"" + propertiesA.getProperty(
"play_client_id",
"5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"
) + "\""
)
} else {
properties.setProperty(
"client_id", "5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"
)
}
buildConfigField(
"String", "ANDROIDACY_CLIENT_ID", "\"" + propertiesA.getProperty("client_id") + "\""
)
buildConfigField(
"java.util.List<String>",
"ENABLED_REPOS",
"java.util.Arrays.asList(\"\")",
)
}
create("fdroid") {
dimension = "type"
applicationIdSuffix = ".fdroid"
// current timestamp of build
buildConfigField("long", "BUILD_TIME", "$timestamp")
// debug http requests. do not set this to true if you care about performance!!!!!
buildConfigField("boolean", "DEBUG_HTTP", "false")
// Latest commit hash as BuildConfig.COMMIT_HASH
buildConfigField("String", "COMMIT_HASH", "\"$gitCommitHash\"")
// Get the current branch name as BuildConfig.BRANCH_NAME
buildConfigField("String", "BRANCH_NAME", "\"$gitBranch\"")
// Get remote url as BuildConfig.REMOTE_URL
buildConfigField("String", "REMOTE_URL", "\"$gitRemote\"")
// Need to disable auto-updater for F-Droid flavor because their inclusion policy
// forbids downloading blobs from third-party websites (and F-Droid APK isn"t signed
// with our keys, so the APK wouldn"t install anyways).
buildConfigField("boolean", "ENABLE_AUTO_UPDATER", "false")
// Disable crash reporting for F-Droid flavor by default
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "false")
buildConfigField("boolean", "DEFAULT_ENABLE_CRASH_REPORTING_PII", "false")
buildConfigField("boolean", "DEFAULT_ENABLE_ANALYTICS", "false")
val properties = Properties()
if (project.rootProject.file("local.properties").exists()) {
properties.load(project.rootProject.file("local.properties").reader())
// grab matomo.url
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"" + properties.getProperty(
"matomo.url", "https://s-api.androidacy.com/matomo.php"
) + "\""
)
} else {
buildConfigField(
"String", "ANALYTICS_ENDPOINT", "\"https://s-api.androidacy.com/matomo.php\""
)
}
buildConfigField("boolean", "ENABLE_PROTECTION", "true")
// Repo with ads or tracking feature are disabled by default for the
// F-Droid flavor. at the same time, the alt repo isn"t particularly trustworthy
buildConfigField(
"java.util.List<String>",
"ENABLED_REPOS",
"java.util.Arrays.asList(\"\")",
)
// Get the androidacy client ID from the androidacy.properties
val propertiesA = Properties()
// If androidacy.properties doesn"t exist, use the fdroid client ID which is limited
// to 50 requests per minute
if (project.rootProject.file("androidacy.properties").exists()) {
propertiesA.load(project.rootProject.file("androidacy.properties").inputStream())
} else {
propertiesA.setProperty(
"client_id", "dQ1p7X8bF14PVJ7wAU6ORVjPB2IeTinsuAZ8Uos6tQiyUdUyIjSyZSmN54QBbaTy"
)
}
buildConfigField(
"String", "ANDROIDACY_CLIENT_ID", "\"" + propertiesA.getProperty(
"client_id", "dQ1p7X8bF14PVJ7wAU6ORVjPB2IeTinsuAZ8Uos6tQiyUdUyIjSyZSmN54QBbaTy"
) + "\""
)
versionNameSuffix = "-froid"
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_19
targetCompatibility = JavaVersion.VERSION_19
}
lint {
disable.add("MissingTranslation")
}
}
sentry {
includeProguardMapping.set(true)
autoUploadProguardMapping.set(hasSentryConfig)
experimentalGuardsquareSupport.set(true)
uploadNativeSymbols.set(hasSentryConfig)
includeNativeSources.set(true)
tracingInstrumentation {
enabled.set(true)
features.set(
setOf(
InstrumentationFeature.DATABASE,
InstrumentationFeature.FILE_IO,
InstrumentationFeature.OKHTTP,
InstrumentationFeature.COMPOSE
)
)
logcat {
enabled.set(true)
minLevel.set(LogcatLevel.WARNING)
}
}
autoInstallation {
enabled.set(true)
sentryVersion.set("6.18.1")
}
includeDependenciesReport.set(true)
}
val abiCodes = mapOf("armeabi-v7a" to 1, "x86" to 2, "x86_64" to 3)
// For per-density APKs, create a similar map:
// val densityCodes = mapOf("mdpi" to 1, "hdpi" to 2, "xhdpi" to 3)
// For each APK output variant, override versionCode with a combination of
// abiCodes * 1000 + variant.versionCode. In this example, variant.versionCode
// is equal to defaultConfig.versionCode. If you configure product flavors that
// define their own versionCode, variant.versionCode uses that value instead.
androidComponents {
onVariants { variant ->
// Assigns a different version code for each output APK
// other than the universal APK.
variant.outputs.forEach { output ->
val name = output.filters.find { it.filterType == ABI }?.identifier
// Stores the value of abiCodes that is associated with the ABI for this variant.
val baseAbiCode = abiCodes[name]
// Because abiCodes.get() returns null for ABIs that are not mapped by ext.abiCodes,
// the following code doesn't override the version code for universal APKs.
// However, because you want universal APKs to have the lowest version code,
// this outcome is desirable.
if (baseAbiCode != null) {
// Assigns the new version code to output.versionCode, which changes the version code
// for only the output APK, not for the variant itself.
val versioCode = output.versionCode.get() as Int
output.versionCode.set(baseAbiCode * 1000 + versioCode)
}
}
}
}
aboutLibraries {
// Specify the additional licenses
additionalLicenses = arrayOf("LGPL_3_0_only", "Apache_2_0")
}
configurations {
// Access all imported libraries
all {
// Exclude all libraries with the following group and module
exclude(group = "org.jetbrains", module = "annotations-java5")
}
}
dependencies {
// UI
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.activity:activity-ktx:1.7.1")
implementation("androidx.emoji2:emoji2:1.3.0")
implementation("androidx.emoji2:emoji2-views-helper:1.3.0")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.webkit:webkit:1.6.1")
implementation("com.google.android.material:material:1.8.0")
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0")
implementation("dev.rikka.rikkax.insets:insets:1.3.0")
implementation("com.github.KieronQuinn:MonetCompat:0.4.1")
implementation("com.github.Fox2Code:FoxCompat:0.2.0")
implementation("com.mikepenz:aboutlibraries:10.6.2")
// Utils
implementation("androidx.work:work-runtime:2.8.1")
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.10")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:5.0.0-alpha.10")
// logging interceptor
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.10")
// Chromium cronet from androidacy
implementation("org.chromium.net:cronet-embedded:108.5359.79")
// protobuf - fixes a crash on some devices
// implementation("com.google.protobuf:protobuf-javalite:3.22.2")
// google guava, maybe fix a bug
implementation("com.google.guava:guava:31.1-jre")
val libsuVersion = "5.0.5"
// The core module that provides APIs to a shell
implementation("com.github.topjohnwu.libsu:core:${libsuVersion}")
// Optional: APIs for creating root services. Depends on ":core"
implementation("com.github.topjohnwu.libsu:service:${libsuVersion}")
// Optional: Provides remote file system support
implementation("com.github.topjohnwu.libsu:io:${libsuVersion}")
implementation("com.github.Fox2Code:RosettaX:1.0.9")
implementation("com.github.Fox2Code:AndroidANSI:1.0.1")
// sentry
implementation("io.sentry:sentry-android:6.18.1")
implementation("io.sentry:sentry-android-timber:6.18.1")
implementation("io.sentry:sentry-android-fragment:6.18.1")
implementation("io.sentry:sentry-android-okhttp:6.18.1")
implementation("io.sentry:sentry-kotlin-extensions:6.18.1")
implementation("io.sentry:sentry-android-ndk:6.18.1")
// Markdown
// TODO: switch to an updated implementation
implementation("io.noties.markwon:core:4.6.2")
implementation("io.noties.markwon:html:4.6.2")
implementation("io.noties.markwon:image:4.6.2")
implementation("io.noties.markwon:syntax-highlight:4.6.2")
implementation("com.google.net.cronet:cronet-okhttp:0.1.0")
implementation("com.caverock:androidsvg:1.4")
implementation("androidx.core:core-ktx:1.10.0")
// timber
implementation("com.jakewharton.timber:timber:5.0.1")
// encryption
implementation("androidx.security:security-crypto:1.1.0-alpha06")
// some utils
implementation("commons-io:commons-io:20030203.000550")
implementation("org.apache.commons:commons-compress:1.23.0")
// analytics
implementation("com.github.matomo-org:matomo-sdk-android:HEAD")
// annotations
implementation("org.jetbrains:annotations-java5:24.0.1")
// debugging
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.10")
// desugaring
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
}
android {
ndkVersion = "25.2.9519653"
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
buildFeatures {
viewBinding = true
buildConfig = true
}
//noinspection GrDeprecatedAPIUsage
buildToolsVersion = "34.0.0 rc3"
@Suppress("DEPRECATION") packagingOptions {
jniLibs {
useLegacyPackaging = true
}
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(19))
}
}

@ -8,9 +8,9 @@
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keepclassmembers class com.fox2code.mmm.androidacy.AndroidacyWebAPI {
public *;
}
# Uncomment this to preserve the line number information for
# debugging stack traces.
@ -50,7 +50,7 @@
static void enableDebugLogging(boolean);
}
-assumevalues class androidx.loader.app.LoaderManagerImpl {
static boolean DEBUG return false;
static boolean DEBUG;
}
# This is just some proguard rules testes, might do a separate lib after
@ -186,3 +186,42 @@
int getSafeInsetTop();
android.graphics.Insets getWaterfallInsets();
}
# Keep all of Cronet API and google's internal classes
-keep class com.google.common.util.concurrent.** { *; }
-keepclassmembers class kotlin.SafePublicationLazyImpl {
java.lang.Object _value;
}
# fix bug with androidx work and future
-keep class androidx.work.impl.utils.futures.* { *; }
# Silence some warnings
-dontwarn android.os.SystemProperties
-dontwarn android.view.ThreadedRenderer
-dontwarn cyanogenmod.providers.CMSettings$Secure
-dontwarn lineageos.providers.LineageSettings$System
-dontwarn lineageos.style.StyleInterface
-dontwarn me.weishu.reflection.Reflection
-dontwarn org.lsposed.hiddenapibypass.HiddenApiBypass
-dontwarn rikka.core.res.ResourcesCompatLayoutInflaterListener
-dontwarn rikka.core.util.ResourceUtils
-dontwarn com.afollestad.materialdialogs.MaterialDialog
-dontwarn com.afollestad.materialdialogs.WhichButton
-dontwarn com.afollestad.materialdialogs.actions.DialogActionExtKt
-dontwarn com.afollestad.materialdialogs.callbacks.DialogCallbackExtKt
-dontwarn com.afollestad.materialdialogs.internal.button.DialogActionButton
-dontwarn com.afollestad.materialdialogs.internal.button.DialogActionButtonLayout
-dontwarn com.afollestad.materialdialogs.internal.main.DialogLayout
-dontwarn com.afollestad.materialdialogs.internal.main.DialogTitleLayout
-dontwarn com.afollestad.materialdialogs.internal.message.DialogContentLayout
-dontwarn com.oracle.svm.core.annotate.AutomaticFeature
-dontwarn com.oracle.svm.core.annotate.Delete
-dontwarn com.oracle.svm.core.annotate.Substitute
-dontwarn com.oracle.svm.core.annotate.TargetClass
-dontwarn com.oracle.svm.core.configure.ResourcesRegistry
-dontwarn javax.lang.model.element.Modifier
-dontwarn org.graalvm.nativeimage.ImageSingletons
-dontwarn org.graalvm.nativeimage.hosted.Feature$BeforeAnalysisAccess
-dontwarn org.graalvm.nativeimage.hosted.Feature
-dontwarn io.sentry.compose.gestures.ComposeGestureTargetLocator

@ -1,26 +0,0 @@
package com.fox2code.mmm;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.fox2code.mmm", appContext.getPackageName());
}
}

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="QueryAllPackagesPermission">
<application android:icon="@mipmap/ic_launcher">
<meta-data android:name="io.sentry.auto-init" android:value="false" />
<meta-data
android:name="io.sentry.dsn"
android:value="https://cdcdb0efca4a42a28df90e4b7f087347@sentry.androidacy.com/2" />
<!-- Sane value, but feel free to lower it -->
<meta-data
android:name="io.sentry.traces.sample-rate"
android:value="0.5" />
<!-- Doesn't actually monitor anything, just used to get the activities the user went through -->
<meta-data
android:name="io.sentry.traces.user-interaction.enable"
android:value="true" />
<!-- Just a screenshot of ONLY the current activity at the time of the crash -->
<meta-data
android:name="io.sentry.attach-screenshot"
android:value="true" />
</application>
</manifest>

@ -1,97 +0,0 @@
package com.fox2code.mmm.sentry;
import android.util.Log;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication;
import java.io.IOException;
import java.io.Writer;
import io.sentry.JsonObjectWriter;
import io.sentry.NoOpLogger;
import io.sentry.Sentry;
import io.sentry.TypeCheckHint;
import io.sentry.android.core.SentryAndroid;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.hints.DiskFlushNotification;
public class SentryMain {
public static final boolean IS_SENTRY_INSTALLED = true;
private static final String TAG = "SentryMain";
public static void initialize(final MainApplication mainApplication) {
SentryAndroid.init(mainApplication, options -> {
// If crash reporting is disabled, stop here.
if (!MainApplication.isCrashReportingEnabled()) {
options.setDsn("");
} else {
options.addIntegration(new FragmentLifecycleIntegration(mainApplication, true, true));
// Sentry sends ABSOLUTELY NO Personally Identifiable Information (PII) by default.
// Already set to false by default, just set it again to make peoples feel safer.
options.setSendDefaultPii(false);
// It just tell if sentry should ping the sentry dsn to tell the app is running.
// This is not needed at all for crash reporting purposes, so disable it.
options.setEnableAutoSessionTracking(false);
// A screenshot of the app itself is only sent if the app crashes, and it only shows the last activity
// In addition, sentry is configured with a trusted third party other than sentry.io, and only trusted people have access to the sentry instance
// Add a callback that will be used before the event is sent to Sentry.
// With this callback, you can modify the event or, when returning null, also discard the event.
options.setBeforeSend((event, hint) -> {
if (BuildConfig.DEBUG) { // Debug sentry events for debug.
StringBuilder stringBuilder = new StringBuilder("Sentry report debug: ");
try {
event.serialize(new JsonObjectWriter(new Writer() {
@Override
public void write(char[] cbuf) {
stringBuilder.append(cbuf);
}
@Override
public void write(String str) {
stringBuilder.append(str);
}
@Override
public void write(char[] chars, int i, int i1) {
stringBuilder.append(chars, i, i1);
}
@Override
public void write(String str, int off, int len) {
stringBuilder.append(str, off, len);
}
@Override
public void flush() {
}
@Override
public void close() {
}
}, 4), NoOpLogger.getInstance());
} catch (IOException ignored) {
}
Log.i(TAG, stringBuilder.toString());
}
if (MainApplication.isCrashReportingEnabled()) {
return event;
} else {
// We need to do this to avoid crash delay on crash when the event is dropped
DiskFlushNotification diskFlushNotification = hint.getAs(
TypeCheckHint.SENTRY_TYPE_CHECK_HINT, DiskFlushNotification.class);
if (diskFlushNotification != null) diskFlushNotification.markFlushed();
return null;
}
});
}
});
}
public static void addSentryBreadcrumb(SentryBreadcrumb sentryBreadcrumb) {
if (MainApplication.isCrashReportingEnabled()) {
Sentry.addBreadcrumb(sentryBreadcrumb.breadcrumb);
}
}
}

@ -1,18 +0,0 @@
package com.fox2code.mmm.sentry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public class SentryBreadcrumb {
public SentryBreadcrumb() {}
public void setType(@Nullable String type) {}
public void setData(@NotNull String key, @Nullable Object value) {
Objects.requireNonNull(key);
}
public void setCategory(@Nullable String category) {}
}

@ -1,11 +0,0 @@
package com.fox2code.mmm.sentry;
import com.fox2code.mmm.MainApplication;
public class SentryMain {
public static final boolean IS_SENTRY_INSTALLED = false;
public static void initialize(MainApplication mainApplication) {}
public static void addSentryBreadcrumb(SentryBreadcrumb sentryBreadcrumb) {}
}

@ -1,17 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="QueryAllPackagesPermission">
tools:ignore="QueryAllPackagesPermission"
tools:targetApi="tiramisu">
<uses-sdk tools:overrideLibrary="io.sentry.android" />
<queries>
<intent>
<action android:name="com.fox2code.mmm.utils.intent.action.OPEN_EXTERNAL" />
</intent>
</queries>
<!-- Wifi is not the only way to get an internet connection -->
<uses-feature android:name="android.hardware.wifi" android:required="false" />
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
<!-- uses webview -->
<uses-feature
android:name="android.software.webview"
android:required="true" />
<!-- uses opengl 1.2 -->
<uses-feature
android:name="android.hardware.opengles.aep"
android:required="false" />
<uses-feature
android:glEsVersion="0x00020000"
android:required="false" />
<!-- Retrieve online modules -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- WebView offline webpage support -->
@ -20,17 +34,25 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Open config apps for applications -->
<uses-permission-sdk-23 android:name="android.permission.QUERY_ALL_PACKAGES" />
<!-- Supposed to fix bugs with old firmware, only requested on pre Marshmallow -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="22" />
<!-- Open and read zips -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Write to external storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Post background notifications -->
<uses-permission-sdk-23 android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Install updates -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!-- Foreground service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- wake lock -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/full_backup_content"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@ -38,13 +60,31 @@
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="@bool/lang_support_rtl"
android:testOnly="false"
android:appCategory="productivity"
android:memtagMode="async"
android:theme="@style/Theme.MagiskModuleManager"
android:usesCleartextTraffic="false"
android:forceDarkAllowed="false"
tools:ignore="ManifestResource"
tools:replace="android:supportsRtl"
tools:targetApi="s">
tools:targetApi="tiramisu">
<activity
android:name=".UpdateActivity"
android:exported="false"
android:theme="@style/Theme.MagiskModuleManager.NoActionBar" />
<activity
android:name=".CrashHandler"
android:exported="false"
android:process=":crash"
android:theme="@style/Theme.MagiskModuleManager.NoActionBar" />
<activity
android:name=".SetupActivity"
android:exported="false"
android:label="@string/title_activity_setup"
android:theme="@style/Theme.MagiskModuleManager.NoActionBar" />
<receiver
android:name="com.fox2code.mmm.background.BackgroundBootListener"
android:name=".background.BackgroundBootListener"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
@ -60,16 +100,19 @@
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name_short"
android:launchMode="singleTask">
android:launchMode="singleTask"
android:theme="@style/Theme.MagiskModuleManager.NoActionBar" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".installer.InstallerActivity"
android:exported="false"
@ -77,24 +120,49 @@
android:parentActivityName=".MainActivity"
android:screenOrientation="portrait"
tools:ignore="LockedOrientationActivity">
<!-- <intent-filter>
<!--
<intent-filter>
<action android:name="${applicationId}.intent.action.INSTALL_MODULE_INTERNAL" />
</intent-filter> -->
</intent-filter>
-->
</activity>
<!-- We can handle zip files -->
<activity
android:name=".utils.ZipFileOpener"
android:exported="true"
android:theme="@style/Theme.MagiskModuleManager.NoActionBar" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/x-zip-compressed" />
<data android:scheme="content" />
</intent-filter>
</activity>
<activity
android:name=".markdown.MarkdownActivity"
android:exported="false"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.MagiskModuleManager" />
android:theme="@style/Theme.MagiskModuleManager.NoActionBar" />
<activity
android:name=".androidacy.AndroidacyActivity"
android:exported="false"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.MagiskModuleManager">
<!-- <intent-filter>
android:theme="@style/Theme.MagiskModuleManager.NoActionBar" >
<!--
<intent-filter>
<action android:name="${applicationId}.intent.action.OPEN_ANDROIDACY_INTERNAL" />
</intent-filter> -->
</intent-filter>
-->
</activity>
<activity
android:name="com.mikepenz.aboutlibraries.ui.LibsActivity"
tools:node="remove" />
@ -113,14 +181,41 @@
</provider>
<provider
android:authorities="${applicationId}.file-provider"
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.file-provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/shared_file_paths" />
</provider>
<meta-data
android:name="io.sentry.auto-init"
android:value="false" />
<meta-data
android:name="io.sentry.dsn"
android:value="https://f35f7f369a254b2ca854cf9593c99da2@sentry.androidacy.com/7" />
<!-- Sane value, but feel free to lower it -->
<meta-data
android:name="io.sentry.traces.sample-rate"
android:value="0.25" />
<!-- Doesn't actually monitor anything, just used to get the activities the user went through -->
<meta-data
android:name="io.sentry.traces.user-interaction.enable"
android:value="true" />
<!-- Just a screenshot of ONLY the current activity at the time of the crash -->
<meta-data
android:name="io.sentry.attach-screenshot"
android:value="true" />
<!-- Just the current activity at the time of the crash -->
<meta-data
android:name="io.sentry.attach-stacktrace"
android:value="true" />
<!-- Performance profiling -->
<meta-data
android:name="io.sentry.traces.profiling.sample-rate"
android:value="0.25" />
</application>
</manifest>
</manifest>

@ -1,4 +1,4 @@
#!/sbin/sh
# shellcheck shell=ash
#################
# Initialization
@ -20,24 +20,24 @@ require_new_magisk() {
# Load util_functions.sh
#########################
OUTFD=$2
ZIPFILE=$3
export OUTFD=$2
export ZIPFILE=$3
mount /data 2>/dev/null
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
. /data/adb/magisk/util_functions.sh
[ $MAGISK_VER_CODE -lt 19000 ] && require_new_magisk
[ "$MAGISK_VER_CODE" -lt 19000 ] && require_new_magisk
# Add grep_get_prop implementation if missing
if ! type grep_get_prop &>/dev/null; then
grep_get_prop() {
local result=$(grep_prop $@)
local result=$(grep_prop "$@")
if [ -z "$result" ]; then
# Fallback to getprop
getprop "$1"
else
echo $result
echo "$result"
fi
}
fi
@ -55,7 +55,7 @@ settings() {
fi
}
if [ $MAGISK_VER_CODE -ge 20400 ] && [ -z "$MMM_MMT_REBORN" ]; then
if [ "$MAGISK_VER_CODE" -ge 20400 ] && [ -z "$MMM_MMT_REBORN" ]; then
# New Magisk have complete installation logic within util_functions.sh
install_module
exit 0
@ -75,9 +75,10 @@ is_legacy_script() {
print_modname() {
local authlen len namelen pounds
namelen=`echo -n $MODNAME | wc -c`
authlen=$((`echo -n $MODAUTH | wc -c` + 3))
[ $namelen -gt $authlen ] && len=$namelen || len=$authlen
# shellcheck disable=SC2006
namelen=`echo -n "$MODNAME" | wc -c`
authlen=$(($(echo -n "$MODAUTH" | wc -c) + 3))
[ "$namelen" -gt $authlen ] && len=$namelen || len=$authlen
len=$((len + 2))
pounds=$(printf "%${len}s" | tr ' ' '*')
ui_print "$pounds"
@ -93,7 +94,7 @@ print_modname() {
abort() {
ui_print "$1"
$BOOTMODE || recovery_cleanup
[ -n $MODPATH ] && rm -rf $MODPATH
[ -n "$MODPATH" ] && rm -rf "$MODPATH"
rm -rf $TMPDIR
exit 1
}
@ -101,7 +102,7 @@ abort() {
rm -rf $TMPDIR 2>/dev/null
mkdir -p $TMPDIR
chcon u:object_r:system_file:s0 $TMPDIR || true
cd $TMPDIR
cd $TMPDIR || exit
# Preperation for flashable zips
setup_flashable
@ -128,14 +129,15 @@ unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2
MODDIRNAME=modules
$BOOTMODE && MODDIRNAME=modules_update
MODULEROOT=$NVBASE/$MODDIRNAME
MODID=`grep_prop id $TMPDIR/module.prop`
MODNAME=`grep_prop name $TMPDIR/module.prop`
MODAUTH=`grep_prop author $TMPDIR/module.prop`
MODID=$(grep_prop id $TMPDIR/module.prop)
MODNAME=$(grep_prop name $TMPDIR/module.prop)
MODAUTH=$(grep_prop author $TMPDIR/module.prop)
MODPATH=$MODULEROOT/$MODID
# Create mod paths
# shellcheck disable=SC2086
rm -rf $MODPATH 2>/dev/null
mkdir -p $MODPATH
mkdir -p "$MODPATH"
##########
# Install
@ -152,22 +154,22 @@ if is_legacy_script; then
on_install
# Custom uninstaller
[ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh
[ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh "$MODPATH"/uninstall.sh
# Skip mount
$SKIPMOUNT && touch $MODPATH/skip_mount
$SKIPMOUNT && touch "$MODPATH"/skip_mount
# prop file
$PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop
$PROPFILE && cp -af $TMPDIR/system.prop "$MODPATH"/system.prop
# Module info
cp -af $TMPDIR/module.prop $MODPATH/module.prop
cp -af $TMPDIR/module.prop "$MODPATH"/module.prop
# post-fs-data scripts
$POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh
$POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh "$MODPATH"/post-fs-data.sh
# service scripts
$LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh
$LATESTARTSERVICE && cp -af $TMPDIR/service.sh "$MODPATH"/service.sh
ui_print "- Setting permissions"
set_permissions
@ -218,46 +220,46 @@ elif [ -n "$MMM_MMT_REBORN" ]; then
else
print_modname
unzip -o "$ZIPFILE" customize.sh -d $MODPATH >&2
unzip -o "$ZIPFILE" customize.sh -d "$MODPATH" >&2
if ! grep -q '^SKIPUNZIP=1$' $MODPATH/customize.sh 2>/dev/null; then
if ! grep -q '^SKIPUNZIP=1$' "$MODPATH"/customize.sh 2>/dev/null; then
ui_print "- Extracting module files"
unzip -o "$ZIPFILE" -x 'META-INF/*' -d $MODPATH >&2
unzip -o "$ZIPFILE" -x 'META-INF/*' -d "$MODPATH" >&2
# Default permissions
set_perm_recursive $MODPATH 0 0 0755 0644
set_perm_recursive "$MODPATH" 0 0 0755 0644
fi
# Load customization script
[ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh
[ -f "$MODPATH"/customize.sh ] && . "$MODPATH"/customize.sh
fi
# Handle replace folders
for TARGET in $REPLACE; do
ui_print "- Replace target: $TARGET"
mktouch $MODPATH$TARGET/.replace
mktouch "$MODPATH""$TARGET"/.replace
done
if $BOOTMODE; then
# Update info for Magisk Manager
mktouch $NVBASE/modules/$MODID/update
rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null
rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null
cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop
mktouch $NVBASE/modules/"$MODID"/update
rm -rf $NVBASE/modules/"$MODID"/remove 2>/dev/null
rm -rf $NVBASE/modules/"$MODID"/disable 2>/dev/null
cp -af "$MODPATH"/module.prop $NVBASE/modules/"$MODID"/module.prop
fi
# Copy over custom sepolicy rules
if ! type copy_sepolicy_rules &>/dev/null; then
if [ -f $MODPATH/sepolicy.rule -a -e $PERSISTDIR ]; then
if [ -f "$MODPATH"/sepolicy.rule -a -e $PERSISTDIR ]; then
ui_print "- Installing custom sepolicy patch"
# Remove old recovery logs (which may be filling partition) to make room
rm -f $PERSISTDIR/cache/recovery/*
PERSISTMOD=$PERSISTDIR/magisk/$MODID
mkdir -p $PERSISTMOD
cp -af $MODPATH/sepolicy.rule $PERSISTMOD/sepolicy.rule || abort "! Insufficient partition size"
mkdir -p "$PERSISTMOD"
cp -af "$MODPATH"/sepolicy.rule "$PERSISTMOD"/sepolicy.rule || abort "! Insufficient partition size"
fi
else
if [ -f $MODPATH/sepolicy.rule ]; then
if [ -f "$MODPATH"/sepolicy.rule ]; then
ui_print "- Installing custom sepolicy rules"
copy_sepolicy_rules
fi
@ -265,9 +267,9 @@ fi
# Remove stuff that doesn't belong to modules and clean up any empty directories
rm -rf \
$MODPATH/system/placeholder $MODPATH/customize.sh \
$MODPATH/README.md $MODPATH/.git* 2>/dev/null
rmdir -p $MODPATH
"$MODPATH"/system/placeholder "$MODPATH"/customize.sh \
"$MODPATH"/README.md "$MODPATH"/.git* 2>/dev/null
rmdir -p "$MODPATH"
#############
# Finalizing

@ -1,15 +1,11 @@
package com.fox2code.mmm;
import android.util.Log;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@ -18,55 +14,55 @@ import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import timber.log.Timber;
// See https://docs.github.com/en/rest/reference/repos#releases
public class AppUpdateManager {
public static final int FLAG_COMPAT_LOW_QUALITY = 0x0001;
public static final int FLAG_COMPAT_NO_EXT = 0x0002;
public static final int FLAG_COMPAT_MAGISK_CMD = 0x0004;
public static final int FLAG_COMPAT_NEED_32BIT = 0x0008;
public static final int FLAG_COMPAT_MALWARE = 0x0010;
public static final int FLAG_COMPAT_NO_ANSI = 0x0020;
public static final int FLAG_COMPAT_FORCE_ANSI = 0x0040;
public static final int FLAG_COMPAT_FORCE_HIDE = 0x0080;
public static final int FLAG_COMPAT_MMT_REBORN = 0x0100;
public static final int FLAG_COMPAT_NO_EXT = 0x0002;
public static final int FLAG_COMPAT_MAGISK_CMD = 0x0004;
public static final int FLAG_COMPAT_NEED_32BIT = 0x0008;
public static final int FLAG_COMPAT_MALWARE = 0x0010;
public static final int FLAG_COMPAT_NO_ANSI = 0x0020;
public static final int FLAG_COMPAT_FORCE_ANSI = 0x0040;
public static final int FLAG_COMPAT_FORCE_HIDE = 0x0080;
public static final int FLAG_COMPAT_MMT_REBORN = 0x0100;
public static final int FLAG_COMPAT_ZIP_WRAPPER = 0x0200;
private static final String TAG = "AppUpdateManager";
public static final String RELEASES_API_URL = "https://api.github.com/repos/Androidacy/MagiskModuleManager/releases/latest";
private static final AppUpdateManager INSTANCE = new AppUpdateManager();
private static final String RELEASES_API_URL =
"https://api.github.com/repos/Fox2Code/FoxMagiskModuleManager/releases";
private static final String COMPAT_API_URL =
"https://api.github.com/repos/Fox2Code/FoxMagiskModuleManager/issues/4";
public static AppUpdateManager getAppUpdateManager() {
return INSTANCE;
}
private final HashMap<String, Integer> compatDataId = new HashMap<>();
private final Object updateLock = new Object();
private final File compatFile;
private String latestRelease;
private String latestPreRelease;
private long lastChecked;
private boolean preReleaseNewer;
private boolean lastCheckSuccess;
private AppUpdateManager() {
this.compatFile = new File(MainApplication.getINSTANCE().getFilesDir(), "compat.txt");
this.latestRelease = MainApplication.getBootSharedPreferences()
.getString("updater_latest_release", BuildConfig.VERSION_NAME);
this.latestPreRelease = MainApplication.getBootSharedPreferences()
.getString("updater_latest_pre_release", BuildConfig.VERSION_NAME);
this.latestRelease = MainApplication.getBootSharedPreferences().getString("updater_latest_release", BuildConfig.VERSION_NAME);
this.lastChecked = 0;
this.preReleaseNewer = true;
if (this.compatFile.isFile()) {
try {
this.parseCompatibilityFlags(new FileInputStream(this.compatFile));
} catch (IOException e) {
e.printStackTrace();
} catch (
IOException ignored) {
}
}
}
public static AppUpdateManager getAppUpdateManager() {
return INSTANCE;
}
public static int getFlagsForModule(String moduleId) {
return INSTANCE.getCompatibilityFlags(moduleId);
}
public static boolean shouldForceHide(String repoId) {
if (BuildConfig.DEBUG || repoId.startsWith("repo_") || repoId.equals("magisk_alt_repo"))
return false;
return !repoId.startsWith("repo_") && (INSTANCE.getCompatibilityFlags(repoId) & FLAG_COMPAT_FORCE_HIDE) != 0;
}
// Return true if should show a notification
public boolean checkUpdate(boolean force) {
if (!BuildConfig.ENABLE_AUTO_UPDATER)
@ -81,166 +77,99 @@ public class AppUpdateManager {
synchronized (this.updateLock) {
if (lastChecked != this.lastChecked)
return this.peekShouldUpdate();
boolean preReleaseNewer = true;
try {
JSONArray releases = new JSONArray(new String(Http.doHttpGet(
RELEASES_API_URL, false), StandardCharsets.UTF_8));
String latestRelease = null, latestPreRelease = null;
for (int i = 0; i < releases.length(); i++) {
JSONObject release = releases.getJSONObject(i);
// Skip invalid entries
if (release.getBoolean("draft")) continue;
boolean preRelease = release.getBoolean("prerelease");
String version = release.getString("tag_name");
if (version.startsWith("v"))
version = version.substring(1);
if (preRelease) {
if (latestPreRelease == null)
latestPreRelease = version;
} else if (latestRelease == null) {
latestRelease = version;
if (latestPreRelease == null)
preReleaseNewer = false;
}
if (latestRelease != null && latestPreRelease != null) {
break; // We read everything we needed to read.
}
JSONObject release = new JSONObject(new String(Http.doHttpGet(RELEASES_API_URL, false), StandardCharsets.UTF_8));
String latestRelease = null;
boolean preRelease = false;
// get latest_release from tag_name translated to int
if (release.has("tag_name")) {
latestRelease = release.getString("tag_name");
preRelease = release.getBoolean("prerelease");
}
if (latestRelease != null)
this.latestRelease = latestRelease;
if (latestPreRelease != null) {
this.latestPreRelease = latestPreRelease;
this.preReleaseNewer = preReleaseNewer;
} else if (!preReleaseNewer) {
this.latestPreRelease = "";
this.preReleaseNewer = false;
Timber.d("Latest release: %s, isPreRelease: %s", latestRelease, preRelease);
if (latestRelease == null)
return false;
if (preRelease) {
this.latestRelease = "99999999"; // prevent updating to pre-release
return false;
}
Log.d(TAG, "Latest release: " + latestRelease);
Log.d(TAG, "Latest pre-release: " + latestPreRelease);
Log.d(TAG, "Latest pre-release newer: " + preReleaseNewer);
this.latestRelease = latestRelease;
this.lastChecked = System.currentTimeMillis();
this.lastCheckSuccess = true;
} catch (Exception ioe) {
this.lastCheckSuccess = false;
Log.e("AppUpdateManager", "Failed to check releases", ioe);
} catch (
Exception ioe) {
Timber.e(ioe);
}
}
return this.peekShouldUpdate();
}
public void checkUpdateCompat() {
if (this.compatFile.exists()) {
long lastUpdate = this.compatFile.lastModified();
if (lastUpdate <= System.currentTimeMillis() &&
lastUpdate + 600_000L > System.currentTimeMillis()) {
return; // Skip update
}
}
compatDataId.clear();
try {
JSONObject object = new JSONObject(new String(Http.doHttpGet(
COMPAT_API_URL, false), StandardCharsets.UTF_8));
if (object.isNull("body")) {
compatDataId.clear();
Files.write(compatFile, new byte[0]);
return;
}
byte[] rawData = object.getString("body")
.getBytes(StandardCharsets.UTF_8);
this.parseCompatibilityFlags(new ByteArrayInputStream(rawData));
Files.write(compatFile, rawData);
if (!BuildConfig.ENABLE_AUTO_UPDATER)
this.lastCheckSuccess = true;
} catch (Exception e) {
if (!BuildConfig.ENABLE_AUTO_UPDATER)
this.lastCheckSuccess = false;
Log.e("AppUpdateManager", "Failed to update compat list", e);
Files.write(compatFile, new byte[0]);
} catch (
IOException e) {
Timber.e(e);
}
// There once lived an implementation that used a GitHub API to get the compatibility flags. It was removed because it was too slow and the API was rate limited.
Timber.w("Remote compatibility data flags are not implemented.");
}
public boolean peekShouldUpdate() {
if (!BuildConfig.ENABLE_AUTO_UPDATER)
if (!BuildConfig.ENABLE_AUTO_UPDATER || BuildConfig.DEBUG)
return false;
return !(BuildConfig.VERSION_NAME.equals(this.latestRelease) ||
(this.preReleaseNewer &&
BuildConfig.VERSION_NAME.equals(this.latestPreRelease)));
// Convert both BuildConfig.VERSION_NAME and latestRelease to int
int currentVersion = 0, latestVersion = 0;
try {
currentVersion = Integer.parseInt(BuildConfig.VERSION_NAME.replaceAll("\\D", ""));
latestVersion = Integer.parseInt(this.latestRelease.replace("v", "").replaceAll("\\D", ""));
} catch (
NumberFormatException ignored) {
}
return currentVersion < latestVersion;
}
public boolean peekHasUpdate() {
if (!BuildConfig.ENABLE_AUTO_UPDATER)
if (!BuildConfig.ENABLE_AUTO_UPDATER || BuildConfig.DEBUG)
return false;
return !BuildConfig.VERSION_NAME.equals(this.preReleaseNewer ?
this.latestPreRelease : this.latestRelease);
}
public boolean isLastCheckSuccess() {
return lastCheckSuccess;
return this.peekShouldUpdate();
}
private void parseCompatibilityFlags(InputStream inputStream) throws IOException {
compatDataId.clear();
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8));
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line;
while ((line = bufferedReader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) continue;
if (line.isEmpty() || line.startsWith("#"))
continue;
int i = line.indexOf('/');
if (i == -1) continue;
if (i == -1)
continue;
int value = 0;
for (String arg : line.substring(i + 1).split(",")) {
switch (arg) {
default:
break;
case "lowQuality":
value |= FLAG_COMPAT_LOW_QUALITY;
break;
case "noExt":
value |= FLAG_COMPAT_NO_EXT;
break;
case "magiskCmd":
value |= FLAG_COMPAT_MAGISK_CMD;
break;
case "need32bit":
value |= FLAG_COMPAT_NEED_32BIT;
break;
case "malware":
value |= FLAG_COMPAT_MALWARE;
break;
case "noANSI":
value |= FLAG_COMPAT_NO_ANSI;
break;
case "forceANSI":
value |= FLAG_COMPAT_FORCE_ANSI;
break;
case "forceHide":
value |= FLAG_COMPAT_FORCE_HIDE;
break;
case "mmtReborn":
value |= FLAG_COMPAT_MMT_REBORN;
break;
case "wrapper":
value |= FLAG_COMPAT_ZIP_WRAPPER;
break;
default -> {
}
case "lowQuality" -> value |= FLAG_COMPAT_LOW_QUALITY;
case "noExt" -> value |= FLAG_COMPAT_NO_EXT;
case "magiskCmd" -> value |= FLAG_COMPAT_MAGISK_CMD;
case "need32bit" -> value |= FLAG_COMPAT_NEED_32BIT;
case "malware" -> value |= FLAG_COMPAT_MALWARE;
case "noANSI" -> value |= FLAG_COMPAT_NO_ANSI;
case "forceANSI" -> value |= FLAG_COMPAT_FORCE_ANSI;
case "forceHide" -> value |= FLAG_COMPAT_FORCE_HIDE;
case "mmtReborn" -> value |= FLAG_COMPAT_MMT_REBORN;
case "wrapper" -> value |= FLAG_COMPAT_ZIP_WRAPPER;
}
}
compatDataId.put(line.substring(0, i), value);
}
bufferedReader.close();
}
public int getCompatibilityFlags(String moduleId) {
Integer compatFlags = compatDataId.get(moduleId);
return compatFlags == null ? 0 : compatFlags;
}
public static int getFlagsForModule(String moduleId) {
return INSTANCE.getCompatibilityFlags(moduleId);
}
public static boolean shouldForceHide(String repoId) {
if (BuildConfig.DEBUG || repoId.startsWith("repo_") ||
repoId.equals("magisk_alt_repo")) return false;
return !repoId.startsWith("repo_") &&
(INSTANCE.getCompatibilityFlags(repoId) &
FLAG_COMPAT_FORCE_HIDE) != 0;
}
}

@ -1,6 +1,8 @@
package com.fox2code.mmm;
public class Constants {
@SuppressWarnings("unused")
public enum Constants {
;
public static final int MAGISK_VER_CODE_FLAT_MODULES = 19000;
public static final int MAGISK_VER_CODE_UTIL_INSTALL = 20400;
public static final int MAGISK_VER_CODE_PATH_SUPPORT = 21000;

@ -0,0 +1,172 @@
package com.fox2code.mmm;
import android.annotation.SuppressLint;
import android.content.ClipboardManager;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import com.fox2code.foxcompat.app.FoxActivity;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textview.MaterialTextView;
import java.io.PrintWriter;
import java.io.StringWriter;
import io.sentry.Sentry;
import io.sentry.UserFeedback;
import timber.log.Timber;
public class CrashHandler extends FoxActivity {
@SuppressLint("RestrictedApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
Timber.i("CrashHandler.onCreate(%s)", savedInstanceState);
// log intent with extras
Timber.d("CrashHandler.onCreate: intent=%s", getIntent());
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crash_handler);
// unlock webview
// set crash_details MaterialTextView to the exception passed in the intent or unknown if null
// convert stacktrace from array to string, and pretty print it (first line is the exception, the rest is the stacktrace, with each line indented by 4 spaces)
MaterialTextView crashDetails = findViewById(R.id.crash_details);
crashDetails.setText("");
// get the exception from the intent
Throwable exception = (Throwable) getIntent().getSerializableExtra("exception");
// get the crashReportingEnabled from the intent
boolean crashReportingEnabled = getIntent().getBooleanExtra("crashReportingEnabled", false);
// if the exception is null, set the crash details to "Unknown"
if (exception == null) {
crashDetails.setText(R.string.crash_details);
} else {
// if the exception is not null, set the crash details to the exception and stacktrace
// stacktrace is an StacktraceElement, so convert it to a string and replace the commas with newlines
StringWriter stringWriter = new StringWriter();
exception.printStackTrace(new PrintWriter(stringWriter));
String stacktrace = stringWriter.toString();
stacktrace = stacktrace.replace(",", "\n ");
crashDetails.setText(getString(R.string.crash_full_stacktrace, stacktrace));
}
String lastEventId = getIntent().getStringExtra("lastEventId");
Timber.d("CrashHandler.onCreate: lastEventId=%s, crashReportingEnabled=%s", lastEventId, crashReportingEnabled);
if (lastEventId == null && crashReportingEnabled) {
// if lastEventId is null, hide the feedback button
findViewById(R.id.feedback).setVisibility(View.GONE);
Timber.d("CrashHandler.onCreate: lastEventId is null but crash reporting is enabled. This may indicate a bug in the crash reporting system.");
} else {
// if lastEventId is not null, show the feedback button
findViewById(R.id.feedback).setVisibility(View.VISIBLE);
}
// disable feedback if sentry is disabled
//noinspection ConstantConditions
if (crashReportingEnabled && lastEventId != null) {
// get name, email, and message fields
EditText name = findViewById(R.id.feedback_name);
EditText email = findViewById(R.id.feedback_email);
EditText description = findViewById(R.id.feedback_message);
// get submit button
findViewById(R.id.feedback_submit).setOnClickListener(v -> {
// require the feedback_message, rest is optional
if (description.getText().toString().equals("")) {
Toast.makeText(this, R.string.sentry_dialogue_empty_message, Toast.LENGTH_LONG).show();
return;
}
// if email or name is empty, use "Anonymous"
final String[] nameString = {name.getText().toString().equals("") ? "Anonymous" : name.getText().toString()};
final String[] emailString = {email.getText().toString().equals("") ? "Anonymous" : email.getText().toString()};
// get sentryException passed in intent
Throwable sentryException = (Throwable) getIntent().getSerializableExtra("sentryException");
new Thread(() -> {
try {
UserFeedback userFeedback;
if (sentryException != null) {
userFeedback = new UserFeedback(Sentry.captureException(sentryException));
// Setups the JSON body
if (nameString[0].equals("")) nameString[0] = "Anonymous";
if (emailString[0].equals("")) emailString[0] = "Anonymous";
userFeedback.setName(nameString[0]);
userFeedback.setEmail(emailString[0]);
userFeedback.setComments(description.getText().toString());
Sentry.captureUserFeedback(userFeedback);
}
Timber.i("Submitted user feedback: name %s email %s comment %s", nameString[0], emailString[0], description.getText().toString());
runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_success, Toast.LENGTH_LONG).show());
// Close the activity
finish();
// start the main activity
startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName()));
} catch (Exception e) {
Timber.e(e, "Failed to submit user feedback");
// Show a toast if the user feedback could not be submitted
runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_failed_toast, Toast.LENGTH_LONG).show());
}
}).start();
});
// get restart button
findViewById(R.id.restart).setOnClickListener(v -> {
// Restart the app
finish();
startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName()));
});
} else {
// disable feedback if sentry is disabled
findViewById(R.id.feedback_name).setEnabled(false);
findViewById(R.id.feedback_email).setEnabled(false);
findViewById(R.id.feedback_message).setEnabled(false);
// fade out all the fields
findViewById(R.id.feedback_name).setAlpha(0.5f);
findViewById(R.id.feedback_email).setAlpha(0.5f);
findViewById(R.id.feedback_message).setAlpha(0.5f);
// fade out the submit button
findViewById(R.id.feedback_submit).setAlpha(0.5f);
// set feedback_text to "Crash reporting is disabled"
((MaterialTextView) findViewById(R.id.feedback_text)).setText(R.string.sentry_enable_nag);
findViewById(R.id.feedback_submit).setOnClickListener(v -> Toast.makeText(this, R.string.sentry_dialogue_disabled, Toast.LENGTH_LONG).show());
// handle restart button
// we have to explicitly enable it because it's disabled by default
findViewById(R.id.restart).setEnabled(true);
findViewById(R.id.restart).setOnClickListener(v -> {
// Restart the app
finish();
startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName()));
});
}
// handle reset button
findViewById(R.id.reset).setOnClickListener(v -> {
// show a confirmation material dialog
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.reset_app);
builder.setMessage(R.string.reset_app_confirmation);
builder.setPositiveButton(R.string.reset, (dialog, which) -> {
// reset the app
MainApplication.getINSTANCE().resetApp();
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
// do nothing
});
builder.show();
});
}
public void copyCrashDetails(View view) {
// change view to a checkmark
view.setBackgroundResource(R.drawable.baseline_check_24);
// copy crash_details to clipboard
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
String crashDetails = ((MaterialTextView) findViewById(R.id.crash_details)).getText().toString();
clipboard.setPrimaryClip(android.content.ClipData.newPlainText("crash_details", crashDetails));
// show a toast
Toast.makeText(this, R.string.crash_details_copied, Toast.LENGTH_LONG).show();
// after 1 second, change the view back to a copy button
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
runOnUiThread(() -> view.setBackgroundResource(R.drawable.baseline_copy_all_24));
}).start();
}
}

File diff suppressed because it is too large Load Diff

@ -1,85 +1,131 @@
package com.fox2code.mmm;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Build;
import android.os.SystemClock;
import android.text.SpannableStringBuilder;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.core.app.NotificationManagerCompat;
import androidx.emoji2.text.DefaultEmojiCompatConfig;
import androidx.emoji2.text.EmojiCompat;
import androidx.emoji2.text.FontRequestEmojiCompatConfig;
import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.FoxApplication;
import com.fox2code.foxcompat.FoxThemeWrapper;
import com.fox2code.foxcompat.internal.FoxProcessExt;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.foxcompat.app.FoxApplication;
import com.fox2code.foxcompat.app.internal.FoxProcessExt;
import com.fox2code.foxcompat.view.FoxThemeWrapper;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.sentry.SentryMain;
import com.fox2code.mmm.utils.GMSProviderInstaller;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.TimberUtils;
import com.fox2code.mmm.utils.io.GMSProviderInstaller;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.sentry.SentryMain;
import com.fox2code.rosettax.LanguageSwitcher;
import com.google.common.hash.Hashing;
import com.topjohnwu.superuser.Shell;
import org.matomo.sdk.Matomo;
import org.matomo.sdk.Tracker;
import org.matomo.sdk.TrackerBuilder;
import org.matomo.sdk.extra.TrackHelper;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import io.noties.markwon.Markwon;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
import io.noties.markwon.syntax.Prism4jTheme;
import io.noties.markwon.syntax.Prism4jThemeDarkula;
import io.noties.markwon.syntax.Prism4jThemeDefault;
import io.noties.markwon.syntax.SyntaxHighlightPlugin;
import io.noties.prism4j.Prism4j;
import io.noties.prism4j.annotations.PrismBundle;
import io.sentry.JsonObjectWriter;
import io.sentry.NoOpLogger;
import io.sentry.android.core.SentryAndroid;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
@PrismBundle(
includeAll = true,
grammarLocatorClassName = ".Prism4jGrammarLocator"
)
public class MainApplication extends FoxApplication
implements androidx.work.Configuration.Provider {
private static final String TAG = "MainApplication";
import io.realm.Realm;
import timber.log.Timber;
public class MainApplication extends FoxApplication implements androidx.work.Configuration.Provider {
// Warning! Locales that don't exist will crash the app
// Anything that is commented out is supported but the translation is not complete to at least 60%
public static final HashSet<String> supportedLocales = new HashSet<>();
private static final String timeFormatString = "dd MMM yyyy"; // Example: 13 july 2001
private static Locale timeFormatLocale =
Resources.getSystem().getConfiguration().locale;
private static SimpleDateFormat timeFormat =
new SimpleDateFormat(timeFormatString, timeFormatLocale);
private static final Shell.Builder shellBuilder;
private static final long secret;
@SuppressLint("RestrictedApi") // Use FoxProcess wrapper helper.
@SuppressLint("RestrictedApi")
// Use FoxProcess wrapper helper.
private static final boolean wrapped = !FoxProcessExt.isRootLoader();
private static SharedPreferences bootSharedPreferences;
private static final ArrayList<String> callers = new ArrayList<>();
public static boolean Iof = false;
private static String SHOWCASE_MODE_TRUE = null;
private static long secret;
private static Locale timeFormatLocale = Resources.getSystem().getConfiguration().getLocales().get(0);
private static SimpleDateFormat timeFormat = new SimpleDateFormat(timeFormatString, timeFormatLocale);
private static String relPackageName = BuildConfig.APPLICATION_ID;
@SuppressLint("StaticFieldLeak")
private static MainApplication INSTANCE;
private static boolean firstBoot;
private static HashMap<Object, Object> mSharedPrefs;
public static String updateCheckBg;
static {
Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create()
.setFlags(Shell.FLAG_REDIRECT_STDERR)
.setTimeout(10).setInitializers(InstallerInitializer.class)
);
secret = new Random().nextLong();
Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create().setFlags(Shell.FLAG_REDIRECT_STDERR | Shell.FLAG_MOUNT_MASTER).setTimeout(15).setInitializers(InstallerInitializer.class));
Random random = new Random();
do {
secret = random.nextLong();
} while (secret == 0);
}
public boolean modulesHaveUpdates = false;
public int updateModuleCount = 0;
public List<String> updateModules = new ArrayList<>();
public boolean isMatomoAllowed;
@StyleRes
private int managerThemeResId = R.style.Theme_MagiskModuleManager;
private FoxThemeWrapper markwonThemeContext;
private Markwon markwon;
private byte[] existingKey;
private Tracker tracker;
private boolean makingNewKey = false;
private boolean isCrashHandler;
public MainApplication() {
if (INSTANCE != null && INSTANCE != this)
throw new IllegalStateException("Duplicate application instance!");
@ -92,16 +138,56 @@ public class MainApplication extends FoxApplication
public static void addSecret(Intent intent) {
ComponentName componentName = intent.getComponent();
String packageName = componentName != null ?
componentName.getPackageName() : intent.getPackage();
if (!BuildConfig.APPLICATION_ID.equalsIgnoreCase(packageName) &&
!relPackageName.equals(packageName)) {
String packageName = componentName != null ? componentName.getPackageName() : intent.getPackage();
if (!BuildConfig.APPLICATION_ID.equalsIgnoreCase(packageName) && !relPackageName.equals(packageName)) {
// Code safeguard, we should never reach here.
throw new IllegalArgumentException("Can't add secret to outbound Intent");
}
intent.putExtra("secret", secret);
}
public static SharedPreferences getSharedPreferences(String name) {
// encryptedSharedPreferences is used
Context mContext = getINSTANCE();
name = name + "x";
if (mSharedPrefs == null) {
Timber.d("Creating shared prefs map");
mSharedPrefs = new HashMap<>();
}
/*
this part is only here because with added encryption, parts of code that were previously calling this over and over again or on each invocation of a method are causing performance issues.
*/
if (BuildConfig.DEBUG) {
// get file, function, and line number
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
// get the caller of this method
StackTraceElement caller = stackTrace[3];
Timber.d("Shared prefs file: %s, caller: %s:%d in %s", name, caller.getFileName(), caller.getLineNumber(), caller.getMethodName());
// add the caller to an array. if the last 3 callers are the same, then we are in a loop, log at error level
callers.add(name + ":" + caller.getLineNumber() + ":" + caller.getMethodName());
// get the last 3 callers
List<String> last3 = callers.subList(Math.max(callers.size() - 3, 0), callers.size());
// if the last 3 callers are the same, then we are in a loop, log at error level
if (last3.size() == 3 && last3.get(0).equals(last3.get(1)) && last3.get(1).equals(last3.get(2))) {
Timber.e("Shared prefs loop detected. File: %s, caller: %s:%d", name, caller.getMethodName(), caller.getLineNumber());
}
}
if (mSharedPrefs.containsKey(name)) {
Timber.d("Returning cached shared prefs");
return (SharedPreferences) mSharedPrefs.get(name);
}
try {
Timber.d("Creating encrypted shared prefs");
MasterKey masterKey = new MasterKey.Builder(mContext).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build();
SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(mContext, name, masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM);
mSharedPrefs.put(name, sharedPreferences);
return sharedPreferences;
} catch (Exception e) {
Timber.e(e, "Failed to create encrypted shared preferences");
return mContext.getSharedPreferences(name, Context.MODE_PRIVATE);
}
}
// Is application wrapped, and therefore must reduce it's feature set.
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isWrapped() {
@ -112,88 +198,85 @@ public class MainApplication extends FoxApplication
return intent != null && intent.getLongExtra("secret", ~secret) == secret;
}
public static SharedPreferences getSharedPreferences() {
return INSTANCE.getSharedPreferences("mmm", MODE_PRIVATE);
}
public static boolean isShowcaseMode() {
return getSharedPreferences().getBoolean("pref_showcase_mode", false);
if (SHOWCASE_MODE_TRUE != null) {
// convert from String to boolean
return Boolean.parseBoolean(SHOWCASE_MODE_TRUE);
}
boolean showcaseMode = getSharedPreferences("mmm").getBoolean("pref_showcase_mode", false);
SHOWCASE_MODE_TRUE = String.valueOf(showcaseMode);
return showcaseMode;
}
public static boolean shouldPreventReboot() {
return getSharedPreferences().getBoolean("pref_prevent_reboot", true);
return getSharedPreferences("mmm").getBoolean("pref_prevent_reboot", true);
}
public static boolean isShowIncompatibleModules() {
return getSharedPreferences().getBoolean("pref_show_incompatible", false);
return getSharedPreferences("mmm").getBoolean("pref_show_incompatible", false);
}
public static boolean isForceDarkTerminal() {
return getSharedPreferences().getBoolean("pref_force_dark_terminal", false);
return getSharedPreferences("mmm").getBoolean("pref_force_dark_terminal", false);
}
public static boolean isTextWrapEnabled() {
return getSharedPreferences().getBoolean("pref_wrap_text", false);
return getSharedPreferences("mmm").getBoolean("pref_wrap_text", false);
}
public static boolean isDohEnabled() {
return getSharedPreferences().getBoolean("pref_dns_over_https", true);
return getSharedPreferences("mmm").getBoolean("pref_dns_over_https", true);
}
public static boolean isMonetEnabled() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
getSharedPreferences().getBoolean("pref_enable_monet", true);
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && getSharedPreferences("mmm").getBoolean("pref_enable_monet", true);
}
public static boolean isBlurEnabled() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
getSharedPreferences().getBoolean("pref_enable_blur", false);
return getSharedPreferences("mmm").getBoolean("pref_enable_blur", false);
}
public static boolean isDeveloper() {
return BuildConfig.DEBUG ||
getSharedPreferences().getBoolean("developer", false);
if (BuildConfig.DEBUG) return true;
return getSharedPreferences("mmm").getBoolean("developer", false);
}
public static boolean isDisableLowQualityModuleFilter() {
return getSharedPreferences().getBoolean("pref_disable_low_quality_module_filter",
false) && isDeveloper();
return getSharedPreferences("mmm").getBoolean("pref_disable_low_quality_module_filter", false) && isDeveloper();
}
public static boolean isUsingMagiskCommand() {
return InstallerInitializer.peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND
&& getSharedPreferences().getBoolean("pref_use_magisk_install_command", false)
&& isDeveloper();
return InstallerInitializer.peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND && getSharedPreferences("mmm").getBoolean("pref_use_magisk_install_command", false) && isDeveloper();
}
public static boolean isBackgroundUpdateCheckEnabled() {
return !wrapped && getSharedPreferences().getBoolean("pref_background_update_check", true);
if (updateCheckBg != null) {
return Boolean.parseBoolean(updateCheckBg);
}
boolean wrapped = isWrapped();
boolean updateCheckBgTemp = !wrapped && getSharedPreferences("mmm").getBoolean("pref_background_update_check", true);
updateCheckBg = String.valueOf(updateCheckBgTemp);
return Boolean.parseBoolean(updateCheckBg);
}
public static boolean isAndroidacyTestMode() {
return isDeveloper() &&
getSharedPreferences().getBoolean("pref_androidacy_test_mode", false);
return isDeveloper() && getSharedPreferences("mmm").getBoolean("pref_androidacy_test_mode", false);
}
public static boolean isFirstBoot() {
return firstBoot;
}
public static boolean hasGottenRootAccess() {
return getSharedPreferences().getBoolean("has_root_access", false);
}
public static void setHasGottenRootAccess(boolean bool) {
getSharedPreferences().edit().putBoolean("has_root_access", bool).apply();
getSharedPreferences("mmm").edit().putBoolean("has_root_access", bool).apply();
}
public static boolean isCrashReportingEnabled() {
return getSharedPreferences().getBoolean("pref_crash_reporting",
BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING && !BuildConfig.DEBUG);
return SentryMain.IS_SENTRY_INSTALLED && getSharedPreferences("mmm").getBoolean("pref_crash_reporting", BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING);
}
public static SharedPreferences getBootSharedPreferences() {
return bootSharedPreferences;
return getSharedPreferences("mmm_boot");
}
public static MainApplication getINSTANCE() {
@ -205,92 +288,51 @@ public class MainApplication extends FoxApplication
return timeFormat.format(new Date(timeStamp));
}
@StyleRes
private int managerThemeResId = R.style.Theme_MagiskModuleManager;
private FoxThemeWrapper markwonThemeContext;
private Markwon markwon;
public static boolean isNotificationPermissionGranted() {
return NotificationManagerCompat.from(INSTANCE).areNotificationsEnabled();
}
public Markwon getMarkwon() {
if (this.markwon != null)
return this.markwon;
if (isCrashHandler) return null;
if (this.markwon != null) return this.markwon;
FoxThemeWrapper contextThemeWrapper = this.markwonThemeContext;
if (contextThemeWrapper == null) {
contextThemeWrapper = this.markwonThemeContext =
new FoxThemeWrapper(this, this.managerThemeResId);
}
Markwon markwon = Markwon.builder(contextThemeWrapper).usePlugin(HtmlPlugin.create())
.usePlugin(SyntaxHighlightPlugin.create(
new Prism4j(new Prism4jGrammarLocator()), new Prism4jSwitchTheme()))
.usePlugin(ImagesPlugin.create().addSchemeHandler(
OkHttpNetworkSchemeHandler.create(Http.getHttpClientWithCache()))).build();
contextThemeWrapper = this.markwonThemeContext = new FoxThemeWrapper(this, this.managerThemeResId);
}
Markwon markwon = Markwon.builder(contextThemeWrapper).usePlugin(HtmlPlugin.create()).usePlugin(ImagesPlugin.create().addSchemeHandler(OkHttpNetworkSchemeHandler.create(Http.getHttpClientWithCache()))).build();
return this.markwon = markwon;
}
public FoxThemeWrapper getMarkwonThemeContext() {
return this.markwonThemeContext;
}
@NonNull
@Override
public androidx.work.Configuration getWorkManagerConfiguration() {
return new androidx.work.Configuration.Builder().build();
}
private class Prism4jSwitchTheme implements Prism4jTheme {
private final Prism4jTheme light = new Prism4jThemeDefault(Color.TRANSPARENT);
private final Prism4jTheme dark = new Prism4jThemeDarkula(Color.TRANSPARENT);
private Prism4jTheme getTheme() {
return isLightTheme() ? this.light : this.dark;
}
@Override
public int background() {
return this.getTheme().background();
}
@Override
public int textColor() {
return this.getTheme().textColor();
}
@Override
public void apply(@NonNull String language, @NonNull Prism4j.Syntax syntax,
@NonNull SpannableStringBuilder builder, int start, int end) {
this.getTheme().apply(language, syntax, builder, start, end);
}
}
@SuppressLint("NonConstantResourceId")
public void setManagerThemeResId(@StyleRes int resId) {
this.managerThemeResId = resId;
if (this.markwonThemeContext != null) {
this.markwonThemeContext.setTheme(resId);
}
this.markwon = null;
}
public void updateTheme() {
@StyleRes int themeResId;
String theme;
boolean monet = isMonetEnabled();
switch (theme = getSharedPreferences().getString("pref_theme", "system")) {
switch (theme = getSharedPreferences("mmm").getString("pref_theme", "system")) {
default:
Log.w("MainApplication", "Unknown theme id: " + theme);
Timber.w("Unknown theme id: %s", theme);
case "system":
themeResId = monet ?
R.style.Theme_MagiskModuleManager_Monet :
R.style.Theme_MagiskModuleManager;
themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet : R.style.Theme_MagiskModuleManager;
break;
case "dark":
themeResId = monet ?
R.style.Theme_MagiskModuleManager_Monet_Dark :
R.style.Theme_MagiskModuleManager_Dark;
themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet_Dark : R.style.Theme_MagiskModuleManager_Dark;
break;
case "black":
themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet_Black : R.style.Theme_MagiskModuleManager_Black;
break;
case "light":
themeResId = monet ?
R.style.Theme_MagiskModuleManager_Monet_Light :
R.style.Theme_MagiskModuleManager_Light;
themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet_Light : R.style.Theme_MagiskModuleManager_Light;
break;
case "transparent_light":
if (monet) {
Timber.tag("MainApplication").w("Monet is not supported for transparent theme");
}
themeResId = R.style.Theme_MagiskModuleManager_Transparent_Light;
break;
}
this.setManagerThemeResId(themeResId);
@ -301,45 +343,106 @@ public class MainApplication extends FoxApplication
return managerThemeResId;
}
@SuppressLint("NonConstantResourceId")
public void setManagerThemeResId(@StyleRes int resId) {
this.managerThemeResId = resId;
if (this.markwonThemeContext != null) {
this.markwonThemeContext.setTheme(resId);
}
this.markwon = null;
}
@SuppressLint("NonConstantResourceId")
public boolean isLightTheme() {
switch (this.managerThemeResId) {
case R.style.Theme_MagiskModuleManager:
case R.style.Theme_MagiskModuleManager_Monet:
return (this.getResources().getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK)
!= Configuration.UI_MODE_NIGHT_YES;
case R.style.Theme_MagiskModuleManager_Monet_Light:
case R.style.Theme_MagiskModuleManager_Light:
return true;
case R.style.Theme_MagiskModuleManager_Monet_Dark:
case R.style.Theme_MagiskModuleManager_Dark:
return false;
default:
return super.isLightTheme();
return switch (getSharedPreferences("mmm").getString("pref_theme", "system")) {
case "system" -> this.isSystemLightTheme();
case "dark", "black" -> false;
default -> true;
};
}
private boolean isSystemLightTheme() {
return (this.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES;
}
@SuppressWarnings("unused")
public boolean isDarkTheme() {
return !this.isLightTheme();
}
@SuppressWarnings("UnusedReturnValue")
public synchronized Tracker getTracker() {
if (tracker == null) {
tracker = TrackerBuilder.createDefault(BuildConfig.ANALYTICS_ENDPOINT, 1).build(Matomo.getInstance(this));
tracker.startNewSession();
tracker.setDispatchInterval(1000);
}
return tracker;
}
@Override
public void onCreate() {
supportedLocales.addAll(Arrays.asList("ar", "bs", "cs", "de", "es-rMX", "fr", "hu", "id", "ja", "nl", "pl", "pt", "pt-rBR", "ro", "ru", "tr", "uk", "zh", "zh-rTW", "en"));
if (INSTANCE == null) INSTANCE = this;
relPackageName = this.getPackageName();
super.onCreate();
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
DynamicColors.applyToActivitiesIfAvailable(this,
new DynamicColorsOptions.Builder().setPrecondition(
(activity, theme) -> isMonetEnabled()).build());
}*/
SharedPreferences sharedPreferences = MainApplication.getSharedPreferences();
SentryMain.initialize(this);
// Initialize Timber
TimberUtils.configTimber();
Timber.i("Starting FoxMMM version %s (%d) - commit %s", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, BuildConfig.COMMIT_HASH);
// Update SSL Ciphers if update is possible
GMSProviderInstaller.installIfNeeded(this);
// detect if we're launching the crashhandler
// get intent that started the crashhandler
Intent intent = getIntent();
if (intent != null) {
if (intent.getClass().getName().equals("com.fox2code.mmm.CrashHandler")) {
isCrashHandler = true;
}
}
if (!isCrashHandler) {
Http.ensureCacheDirs();
Http.ensureURLHandler(getApplicationContext());
}
Timber.d("Initializing FoxMMM");
Timber.d("Started from background: %s", !isInForeground());
Timber.d("FoxMMM is running in debug mode");
Timber.d("Initializing Realm");
Realm.init(this);
Timber.d("Initialized Realm");
// analytics
Timber.d("Initializing matomo");
isMatomoAllowed = isMatomoAllowed();
getTracker();
if (!isMatomoAllowed) {
Timber.d("Matomo is not allowed");
tracker.setOptOut(true);
} else {
tracker.setOptOut(false);
}
if (getSharedPreferences("matomo").getBoolean("install_tracked", false)) {
TrackHelper.track().download().with(MainApplication.getINSTANCE().getTracker());
Timber.d("Sent install event to matomo");
getSharedPreferences("matomo").edit().putBoolean("install_tracked", true).apply();
} else {
Timber.d("Matomo already has install");
}
try {
@SuppressLint("PackageManagerGetSignatures") Signature[] s = this.getPackageManager().getPackageInfo(this.getPackageName(), PackageManager.GET_SIGNATURES).signatures;
@SuppressWarnings("SpellCheckingInspection") String[] osh = new String[]{"7bec7c4462f4aac616612d9f56a023ee3046e83afa956463b5fab547fd0a0be6", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"};
//noinspection SpellCheckingInspection
String oosh = Hashing.sha256().hashBytes(s[0].toByteArray()).toString();
Iof = Arrays.asList(osh).contains(oosh);
} catch (PackageManager.NameNotFoundException ignored) {
}
SharedPreferences sharedPreferences = MainApplication.getSharedPreferences("mmm");
// We are only one process so it's ok to do this
SharedPreferences bootPrefs = MainApplication.bootSharedPreferences =
this.getSharedPreferences("mmm_boot", MODE_PRIVATE);
SharedPreferences bootPrefs = MainApplication.getSharedPreferences("mmm_boot");
long lastBoot = System.currentTimeMillis() - SystemClock.elapsedRealtime();
long lastBootPrefs = bootPrefs.getLong("last_boot", 0);
if (lastBootPrefs == 0 || Math.abs(lastBoot - lastBootPrefs) > 100) {
boolean firstBoot = sharedPreferences.getBoolean("first_boot", true);
bootPrefs.edit().clear().putLong("last_boot", lastBoot)
.putBoolean("first_boot", firstBoot).apply();
bootPrefs.edit().clear().putLong("last_boot", lastBoot).putBoolean("first_boot", firstBoot).apply();
if (firstBoot) {
sharedPreferences.edit().putBoolean("first_boot", false).apply();
}
@ -350,24 +453,34 @@ public class MainApplication extends FoxApplication
// Force initialize language early.
new LanguageSwitcher(this);
this.updateTheme();
// Update SSL Ciphers if update is possible
GMSProviderInstaller.installIfNeeded(this);
// Update emoji config
FontRequestEmojiCompatConfig fontRequestEmojiCompatConfig =
DefaultEmojiCompatConfig.create(this);
FontRequestEmojiCompatConfig fontRequestEmojiCompatConfig = DefaultEmojiCompatConfig.create(this);
if (fontRequestEmojiCompatConfig != null) {
fontRequestEmojiCompatConfig.setReplaceAll(true);
fontRequestEmojiCompatConfig
.setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
fontRequestEmojiCompatConfig.setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
EmojiCompat emojiCompat = EmojiCompat.init(fontRequestEmojiCompatConfig);
new Thread(() -> {
Log.d("MainApplication", "Loading emoji compat...");
Timber.i("Loading emoji compat...");
emojiCompat.load();
Log.d("MainApplication", "Emoji compat loaded!");
Timber.i("Emoji compat loaded!");
}, "Emoji compat init.").start();
}
if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) {
Timber.w("Androidacy client id is empty! Please set it in androidacy.properties. Will not enable Androidacy.");
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean("pref_androidacy_repo_enabled", false);
Timber.w("ANDROIDACY_CLIENT_ID is empty, disabling AndroidacyRepoData 1");
editor.apply();
}
}
SentryMain.initialize(this);
private boolean isMatomoAllowed() {
return getSharedPreferences("mmm").getBoolean("pref_analytics_enabled", BuildConfig.DEFAULT_ENABLE_ANALYTICS);
}
@SuppressWarnings("unused")
private Intent getIntent() {
return this.getPackageManager().getLaunchIntentForPackage(this.getPackageName());
}
@Override
@ -384,12 +497,256 @@ public class MainApplication extends FoxApplication
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
Locale newTimeFormatLocale = newConfig.locale;
Locale newTimeFormatLocale = newConfig.getLocales().get(0);
if (timeFormatLocale != newTimeFormatLocale) {
timeFormatLocale = newTimeFormatLocale;
timeFormat = new SimpleDateFormat(
timeFormatString, timeFormatLocale);
timeFormat = new SimpleDateFormat(timeFormatString, timeFormatLocale);
}
super.onConfigurationChanged(newConfig);
}
// getDataDir wrapper with optional path parameter
public File getDataDirWithPath(String path) {
File dataDir = this.getDataDir();
// for path with / somewhere in the middle, its a subdirectory
if (path != null) {
if (path.startsWith("/")) path = path.substring(1);
if (path.endsWith("/")) path = path.substring(0, path.length() - 1);
if (path.contains("/")) {
String[] dirs = path.split("/");
for (String dir : dirs) {
dataDir = new File(dataDir, dir);
// make sure the directory exists
if (!dataDir.exists()) {
if (!dataDir.mkdirs()) {
if (BuildConfig.DEBUG)
Timber.w("Failed to create directory %s", dataDir);
}
}
}
} else {
dataDir = new File(dataDir, path);
// create the directory if it doesn't exist
if (!dataDir.exists()) {
if (!dataDir.mkdirs()) {
if (BuildConfig.DEBUG) Timber.w("Failed to create directory %s", dataDir);
}
}
}
return dataDir;
} else {
throw new IllegalArgumentException("Path cannot be null");
}
}
@SuppressLint("RestrictedApi")
// view is nullable because it's called from xml
public void resetApp() {
// cant show a dialog because android is throwing a fit so here's hoping anybody who calls this method is otherwise confirming that the user wants to reset the app
Timber.w("Resetting app...");
// recursively delete the app's data
((ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE)).clearApplicationUserData();
}
public boolean isInForeground() {
// determine if the app is in the foreground
ActivityManager activityManager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
if (appProcesses == null) {
Timber.d("appProcesses is null");
return false;
}
final String packageName = this.getPackageName();
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
Timber.d("Process: %s, Importance: %d", appProcess.processName, appProcess.importance);
if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName.equals(packageName)) {
return true;
}
}
return false;
}
// returns if background execution is restricted
@SuppressWarnings("unused")
public boolean isBackgroundRestricted() {
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return am.isBackgroundRestricted();
} else {
return false;
}
}
// Create a key to encrypt a realm and save it securely in the keystore
public byte[] getKey() {
if (makingNewKey) {
// sleep until the key is made
while (makingNewKey) try {
//noinspection BusyWait
Thread.sleep(100);
} catch (InterruptedException ignored) {
// silence is bliss
}
}
// attempt to read the existingKey property
if (existingKey != null) {
return existingKey;
}
// check if we have a key already
SharedPreferences sharedPreferences = MainApplication.getSharedPreferences("realm_key");
if (sharedPreferences.contains("iv_and_encrypted_key")) {
return getExistingKey();
} else {
makingNewKey = true;
}
// open a connection to the android keystore
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException |
IOException e) {
Timber.v("Failed to open the keystore.");
throw new RuntimeException(e);
}
// create a securely generated random asymmetric RSA key
byte[] realmKey = new byte[Realm.ENCRYPTION_KEY_LENGTH];
new SecureRandom().nextBytes(realmKey);
// create a cipher that uses AES encryption -- we'll use this to encrypt our key
Cipher cipher;
try {
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
Timber.e("Failed to create a cipher.");
throw new RuntimeException(e);
}
Timber.v("Cipher created.");
// generate secret key
KeyGenerator keyGenerator;
try {
keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
Timber.e("Failed to access the key generator.");
throw new RuntimeException(e);
}
KeyGenParameterSpec keySpec = new KeyGenParameterSpec.Builder("realm_key", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT).setBlockModes(KeyProperties.BLOCK_MODE_CBC).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7).build();
try {
keyGenerator.init(keySpec);
} catch (InvalidAlgorithmParameterException e) {
Timber.e("Failed to generate a secret key.");
throw new RuntimeException(e);
}
Timber.v("Secret key generated.");
keyGenerator.generateKey();
Timber.v("Secret key stored in the keystore.");
// access the generated key in the android keystore, then
// use the cipher to create an encrypted version of the key
byte[] initializationVector;
byte[] encryptedKeyForRealm;
try {
SecretKey secretKey = (SecretKey) keyStore.getKey("realm_key", null);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
encryptedKeyForRealm = cipher.doFinal(realmKey);
initializationVector = cipher.getIV();
} catch (InvalidKeyException | UnrecoverableKeyException | NoSuchAlgorithmException |
KeyStoreException | BadPaddingException | IllegalBlockSizeException e) {
Timber.e("Failed encrypting the key with the secret key.");
throw new RuntimeException(e);
}
// keep the encrypted key in shared preferences
// to persist it across application runs
byte[] initializationVectorAndEncryptedKey = new byte[Integer.BYTES + initializationVector.length + encryptedKeyForRealm.length];
ByteBuffer buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.putInt(initializationVector.length);
buffer.put(initializationVector);
buffer.put(encryptedKeyForRealm);
Timber.d("Created all keys successfully.");
MainApplication.getSharedPreferences("realm_key").edit().putString("iv_and_encrypted_key", Base64.encodeToString(initializationVectorAndEncryptedKey, Base64.NO_WRAP)).apply();
Timber.d("Saved the encrypted key in shared preferences.");
makingNewKey = false;
return realmKey; // pass to a realm configuration via encryptionKey()
}
// Access the encrypted key in the keystore, decrypt it with the secret,
// and use it to open and read from the realm again
public byte[] getExistingKey() {
// attempt to read the existingKey property
if (existingKey != null) {
return existingKey;
}
// open a connection to the android keystore
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException |
IOException e) {
Timber.e("Failed to open the keystore.");
throw new RuntimeException(e);
}
Timber.v("Keystore opened.");
// access the encrypted key that's stored in shared preferences
byte[] initializationVectorAndEncryptedKey = Base64.decode(MainApplication.getSharedPreferences("realm_key").getString("iv_and_encrypted_key", null), Base64.DEFAULT);
Timber.d("Retrieved the encrypted key from shared preferences. Key length: %d", initializationVectorAndEncryptedKey.length);
ByteBuffer buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey);
buffer.order(ByteOrder.BIG_ENDIAN);
// extract the length of the initialization vector from the buffer
int initializationVectorLength = buffer.getInt();
// extract the initialization vector based on that length
byte[] initializationVector = new byte[initializationVectorLength];
buffer.get(initializationVector);
// extract the encrypted key
byte[] encryptedKey = new byte[initializationVectorAndEncryptedKey.length - Integer.BYTES - initializationVectorLength];
buffer.get(encryptedKey);
Timber.d("Got key from shared preferences.");
// create a cipher that uses AES encryption to decrypt our key
Cipher cipher;
try {
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
Timber.e("Failed to create cipher.");
throw new RuntimeException(e);
}
// decrypt the encrypted key with the secret key stored in the keystore
byte[] decryptedKey;
try {
final SecretKey secretKey = (SecretKey) keyStore.getKey("realm_key", null);
final IvParameterSpec initializationVectorSpec = new IvParameterSpec(initializationVector);
cipher.init(Cipher.DECRYPT_MODE, secretKey, initializationVectorSpec);
decryptedKey = cipher.doFinal(encryptedKey);
} catch (InvalidKeyException e) {
Timber.e("Failed to decrypt. Invalid key.");
throw new RuntimeException(e);
} catch (UnrecoverableKeyException | NoSuchAlgorithmException | BadPaddingException |
KeyStoreException | IllegalBlockSizeException |
InvalidAlgorithmParameterException e) {
Timber.e("Failed to decrypt the encrypted realm key with the secret key.");
throw new RuntimeException(e);
}
// set property on MainApplication to indicate that the key has been accessed
existingKey = decryptedKey;
return decryptedKey; // pass to a realm configuration via encryptionKey()
}
public void resetUpdateModule() {
modulesHaveUpdates = false;
updateModuleCount = 0;
updateModules = new ArrayList<>();
}
public static class ReleaseTree extends Timber.Tree {
@Override
protected void log(int priority, String tag, @NonNull String message, Throwable t) {
// basically silently drop all logs below error, and write the rest to logcat
if (priority >= Log.ERROR) {
if (t != null) {
Log.println(priority, tag, message);
t.printStackTrace();
} else {
Log.println(priority, tag, message);
}
}
}
}
}

@ -1,6 +1,5 @@
package com.fox2code.mmm;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
@ -8,25 +7,36 @@ import androidx.annotation.AttrRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.module.ModuleViewListBuilder;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.net.Http;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import timber.log.Timber;
interface NotificationTypeCst {
String TAG = "NotificationType";
}
public enum NotificationType implements NotificationTypeCst {
DEBUG(R.string.debug_build, R.drawable.ic_baseline_bug_report_24, com.google.android.material.R.attr.colorSecondary, com.google.android.material.R.attr.colorOnSecondary) {
@Override
public boolean shouldRemove() {
return !BuildConfig.DEBUG;
}
},
SHOWCASE_MODE(R.string.showcase_mode, R.drawable.ic_baseline_lock_24,
R.attr.colorPrimary, R.attr.colorOnPrimary) {
androidx.appcompat.R.attr.colorPrimary, com.google.android.material.R.attr.colorOnPrimary) {
@Override
public boolean shouldRemove() {
return !MainApplication.isShowcaseMode();
@ -52,9 +62,7 @@ public enum NotificationType implements NotificationTypeCst {
return InstallerInitializer.getErrorNotification() != this;
}
},
MAGISK_OUTDATED(R.string.magisk_outdated, R.drawable.ic_baseline_update_24, v -> {
IntentHelper.openUrl(v.getContext(), "https://github.com/topjohnwu/Magisk/releases");
}) {
MAGISK_OUTDATED(R.string.magisk_outdated, R.drawable.ic_baseline_update_24, v -> IntentHelper.openUrl(v.getContext(), "https://github.com/topjohnwu/Magisk/releases")) {
@Override
public boolean shouldRemove() {
return InstallerInitializer.peekMagiskPath() == null ||
@ -65,8 +73,22 @@ public enum NotificationType implements NotificationTypeCst {
NO_INTERNET(R.string.fail_internet, R.drawable.ic_baseline_cloud_off_24) {
@Override
public boolean shouldRemove() {
return AppUpdateManager.getAppUpdateManager().isLastCheckSuccess() ||
RepoManager.getINSTANCE().hasConnectivity();
return RepoManager.getINSTANCE().hasConnectivity();
}
},
REPO_UPDATE_FAILED(R.string.repo_update_failed, R.drawable.ic_baseline_cloud_off_24) {
@Override
public boolean shouldRemove() {
return RepoManager.getINSTANCE().isLastUpdateSuccess();
}
},
NEED_CAPTCHA_ANDROIDACY(R.string.androidacy_need_captcha, R.drawable.ic_baseline_refresh_24, v ->
IntentHelper.openUrlAndroidacy(v.getContext(),
"https://" + Http.needCaptchaAndroidacyHost() + "/", false)) {
@Override
public boolean shouldRemove() {
return !RepoManager.isAndroidacyRepoEnabled()
|| !Http.needCaptchaAndroidacy();
}
},
NO_WEB_VIEW(R.string.no_web_view, R.drawable.ic_baseline_android_24) {
@ -76,17 +98,15 @@ public enum NotificationType implements NotificationTypeCst {
}
},
UPDATE_AVAILABLE(R.string.app_update_available, R.drawable.ic_baseline_system_update_24,
R.attr.colorPrimary, R.attr.colorOnPrimary, v -> {
IntentHelper.openUrl(v.getContext(),
"https://github.com/Fox2Code/FoxMagiskModuleManager/releases");
}, false) {
androidx.appcompat.R.attr.colorPrimary, com.google.android.material.R.attr.colorOnPrimary, v -> IntentHelper.openUrl(v.getContext(),
"https://github.com/Androidacy/MagiskModuleManager/releases"), false) {
@Override
public boolean shouldRemove() {
return !AppUpdateManager.getAppUpdateManager().peekShouldUpdate();
}
},
INSTALL_FROM_STORAGE(R.string.install_from_storage, R.drawable.ic_baseline_storage_24,
R.attr.colorBackgroundFloating, R.attr.colorOnBackground, v -> {
androidx.appcompat.R.attr.colorBackgroundFloating, com.google.android.material.R.attr.colorOnBackground, v -> {
FoxActivity compatActivity = FoxActivity.getFoxActivity(v);
final File module = new File(compatActivity.getCacheDir(),
"installer" + File.separator + "module.zip");
@ -99,7 +119,7 @@ public enum NotificationType implements NotificationTypeCst {
}
if (needPatch(d)) {
if (d.exists() && !d.delete())
Log.w(TAG, "Failed to delete non module zip");
Timber.w("Failed to delete non module zip");
Toast.makeText(compatActivity,
R.string.invalid_format, Toast.LENGTH_SHORT).show();
} else {
@ -111,7 +131,7 @@ public enum NotificationType implements NotificationTypeCst {
}
} catch (IOException ignored) {
if (d.exists() && !d.delete())
Log.w(TAG, "Failed to delete invalid module");
Timber.w("Failed to delete invalid module");
Toast.makeText(compatActivity,
R.string.invalid_format, Toast.LENGTH_SHORT).show();
}
@ -132,12 +152,37 @@ public enum NotificationType implements NotificationTypeCst {
}
};
private static boolean needPatch(File target) throws IOException {
public static boolean needPatch(File target) {
try (ZipFile zipFile = new ZipFile(target)) {
return zipFile.getEntry("module.prop") == null &&
zipFile.getEntry("anykernel.sh") == null &&
zipFile.getEntry("META-INF/com/google/android/magisk/module.prop") == null;
boolean validEntries = zipFile.getEntry("module.prop") != null;
// ensure there's no anykernel.sh
validEntries &= zipFile.getEntry("anykernel.sh") == null;
if (validEntries) {
// Ensure id of module is not empty and matches ^[a-zA-Z][a-zA-Z0-9._-]+$ regex
// We need to get the module.prop and parse the id= line
ZipEntry moduleProp = zipFile.getEntry("module.prop");
// Parse the module.prop
if (moduleProp != null) {
// Find the line with id=, and check if it matches the regex
try (BufferedReader reader = new BufferedReader(new InputStreamReader(zipFile.getInputStream(moduleProp)))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("id=")) {
String id = line.substring(3);
return id.isEmpty() || !id.matches("^[a-zA-Z][a-zA-Z0-9._-]+$");
}
}
}
} else {
return true;
}
} else {
return true;
}
} catch (IOException e) {
return true;
}
return false;
}
@StringRes
@ -152,11 +197,11 @@ public enum NotificationType implements NotificationTypeCst {
public final boolean special;
NotificationType(@StringRes int textId, int iconId) {
this(textId, iconId, R.attr.colorError, R.attr.colorOnPrimary);
this(textId, iconId, androidx.appcompat.R.attr.colorError, com.google.android.material.R.attr.colorOnPrimary);
}
NotificationType(@StringRes int textId, int iconId, View.OnClickListener onClickListener) {
this(textId, iconId, R.attr.colorError, R.attr.colorOnPrimary, onClickListener);
this(textId, iconId, androidx.appcompat.R.attr.colorError, com.google.android.material.R.attr.colorOnPrimary, onClickListener);
}
NotificationType(@StringRes int textId, int iconId, int backgroundAttr, int foregroundAttr) {
@ -169,7 +214,7 @@ public enum NotificationType implements NotificationTypeCst {
}
NotificationType(@StringRes int textId, int iconId, int backgroundAttr, int foregroundAttr,
View.OnClickListener onClickListener, boolean special) {
View.OnClickListener onClickListener, @SuppressWarnings("SameParameterValue") boolean special) {
this.textId = textId;
this.iconId = iconId;
this.backgroundAttr = backgroundAttr;
@ -179,6 +224,11 @@ public enum NotificationType implements NotificationTypeCst {
}
public boolean shouldRemove() {
// By default, remove the notification`
return false;
}
public final void autoAdd(ModuleViewListBuilder moduleViewListBuilder) {
if (!shouldRemove()) moduleViewListBuilder.addNotification(this);
}
}

@ -0,0 +1,396 @@
package com.fox2code.mmm;
import static com.fox2code.mmm.utils.IntentHelper.getActivity;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import android.webkit.CookieManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.mmm.databinding.ActivitySetupBinding;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.utils.realm.ReposList;
import com.fox2code.rosettax.LanguageActivity;
import com.fox2code.rosettax.LanguageSwitcher;
import com.google.android.material.bottomnavigation.BottomNavigationItemView;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.materialswitch.MaterialSwitch;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import timber.log.Timber;
public class SetupActivity extends FoxActivity implements LanguageActivity {
private int cachedTheme;
private boolean realmDatabasesCreated;
@SuppressLint({"ApplySharedPref", "RestrictedApi"})
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setTitle(R.string.setup_title);
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, 0);
createFiles();
disableUpdateActivityForFdroidFlavor();
// Set theme
SharedPreferences prefs = MainApplication.getSharedPreferences("mmm");
switch (prefs.getString("theme", "system")) {
case "light" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Light);
case "dark" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Dark);
case "system" -> setTheme(R.style.Theme_MagiskModuleManager_Monet);
case "black" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Black);
case "transparent_light" ->
setTheme(R.style.Theme_MagiskModuleManager_Transparent_Light);
}
ActivitySetupBinding binding = ActivitySetupBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
View view = binding.getRoot();
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).setChecked(BuildConfig.ENABLE_AUTO_UPDATER);
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).setChecked(BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING);
// pref_crash_reporting_pii
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting_pii))).setChecked(BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING_PII);
// pref_analytics_enabled
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_app_analytics))).setChecked(BuildConfig.DEFAULT_ENABLE_ANALYTICS);
// assert that both switches match the build config on debug builds
if (BuildConfig.DEBUG) {
assert ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).isChecked() == BuildConfig.ENABLE_AUTO_UPDATER;
assert ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).isChecked() == BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING;
}
// Repos are a little harder, as the enabled_repos build config is an arraylist
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_androidacy_repo))).setChecked(BuildConfig.ENABLED_REPOS.contains("androidacy_repo"));
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_magisk_alt_repo))).setChecked(BuildConfig.ENABLED_REPOS.contains("magisk_alt_repo"));
// On debug builds, log when a switch is toggled
if (BuildConfig.DEBUG) {
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).setOnCheckedChangeListener((buttonView, isChecked) -> Timber.i("Automatic update Check: %s", isChecked));
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).setOnCheckedChangeListener((buttonView, isChecked) -> Timber.i("Crash Reporting: %s", isChecked));
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting_pii))).setOnCheckedChangeListener((buttonView, isChecked) -> Timber.i("Crash Reporting PII: %s", isChecked));
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_androidacy_repo))).setOnCheckedChangeListener((buttonView, isChecked) -> Timber.i("Androidacy Repo: %s", isChecked));
((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_magisk_alt_repo))).setOnCheckedChangeListener((buttonView, isChecked) -> Timber.i("Magisk Alt Repo: %s", isChecked));
}
// Setup popup dialogue for the setup_theme_button
MaterialButton themeButton = view.findViewById(R.id.setup_theme_button);
themeButton.setOnClickListener(v -> {
// Create a new dialog for the theme picker
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.setup_theme_title);
// Create a new array of theme names (system, light, dark, black, transparent light)
String[] themeNames = new String[]{getString(R.string.theme_system), getString(R.string.theme_light), getString(R.string.theme_dark), getString(R.string.theme_black), getString(R.string.theme_transparent_light)};
// Create a new array of theme values (system, light, dark, black, transparent_light)
String[] themeValues = new String[]{"system", "light", "dark", "black", "transparent_light"};
// if pref_theme is set, check the relevant theme_* menu item, otherwise check the default (theme_system)
String prefTheme = prefs.getString("pref_theme", "system");
int checkedItem = 0;
switch (prefTheme) {
case "system":
break;
case "light":
checkedItem = 1;
break;
case "dark":
checkedItem = 2;
break;
case "black":
checkedItem = 3;
break;
case "transparent_light":
checkedItem = 4;
break;
}
builder.setCancelable(true);
// Create the dialog
builder.setSingleChoiceItems(themeNames, checkedItem, (dialog, which) -> {
// Set the theme
prefs.edit().putString("pref_theme", themeValues[which]).commit();
// Dismiss the dialog
dialog.dismiss();
// Set the theme
UiThreadHandler.handler.postDelayed(() -> {
switch (prefs.getString("pref_theme", "system")) {
case "light" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Light);
case "dark" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Dark);
case "system" -> setTheme(R.style.Theme_MagiskModuleManager_Monet);
case "black" -> setTheme(R.style.Theme_MagiskModuleManager_Monet_Black);
case "transparent_light" ->
setTheme(R.style.Theme_MagiskModuleManager_Transparent_Light);
}
// restart the activity because switching to transparent pisses the rendering engine off
Intent intent = new Intent(this, SetupActivity.class);
finish();
// ensure intent originates from the same package
intent.setPackage(getPackageName());
startActivity(intent);
}, 100);
});
builder.show();
});
// Setup language selector
MaterialButton languageSelector = view.findViewById(R.id.setup_language_button);
languageSelector.setOnClickListener(preference -> {
LanguageSwitcher ls = new LanguageSwitcher(Objects.requireNonNull(getActivity(this)));
ls.setSupportedStringLocales(MainApplication.supportedLocales);
ls.showChangeLanguageDialog((FragmentActivity) getActivity(this));
});
// Set up the buttons
// Setup button
BottomNavigationItemView setupButton = view.findViewById(R.id.setup_finish);
// on clicking setup_agree_eula, enable the setup button if it's checked, if it's not, disable it
MaterialSwitch agreeEula = view.findViewById(R.id.setup_agree_eula);
agreeEula.setOnCheckedChangeListener((buttonView, isChecked) -> setupButton.setEnabled(isChecked));
setupButton.setOnClickListener(v -> {
Timber.i("Setup button clicked");
// get instance of editor
Timber.d("Saving preferences");
SharedPreferences.Editor editor = prefs.edit();
Timber.d("Got editor: %s", editor);
// Set the Automatic update check pref
editor.putBoolean("pref_background_update_check", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).isChecked());
// require wifi pref
editor.putBoolean("pref_background_update_check_wifi", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check_require_wifi))).isChecked());
// Set the crash reporting pref
editor.putBoolean("pref_crash_reporting", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).isChecked());
// Set the crash reporting PII pref
editor.putBoolean("pref_crash_reporting_pii", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting_pii))).isChecked());
editor.putBoolean("pref_analytics_enabled", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_app_analytics))).isChecked());
Timber.d("Saving preferences");
// Set the repos in the ReposList realm db
RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
boolean androidacyRepo = ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_androidacy_repo))).isChecked();
boolean magiskAltRepo = ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_magisk_alt_repo))).isChecked();
Realm realm = Realm.getInstance(realmConfig);
Timber.d("Realm instance: %s", realm);
if (realm.isInTransaction()) {
realm.commitTransaction();
Timber.d("Committed last unfinished transaction");
}
// check if instance has been closed
if (realm.isClosed()) {
Timber.d("Realm instance was closed, reopening");
realm = Realm.getInstance(realmConfig);
}
realm.executeTransactionAsync(r -> {
Timber.d("Realm transaction started");
Objects.requireNonNull(r.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst()).setEnabled(androidacyRepo);
Objects.requireNonNull(r.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst()).setEnabled(magiskAltRepo);
Timber.d("Realm transaction committing");
// commit the changes
r.commitTransaction();
r.close();
Timber.d("Realm transaction committed");
});
editor.putString("last_shown_setup", "v2");
// Commit the changes
editor.commit();
// sleep to allow the realm transaction to finish
try {
Thread.sleep(250);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Log the changes
Timber.d("Setup finished. Preferences: %s", prefs.getAll());
Timber.d("Androidacy repo: %s", androidacyRepo);
Timber.d("Magisk Alt repo: %s", magiskAltRepo);
// log last shown setup
Timber.d("Last shown setup: %s", prefs.getString("last_shown_setup", "v0"));
// Restart the activity
MainActivity.doSetupRestarting = true;
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_IMMUTABLE);
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
android.os.Process.killProcess(android.os.Process.myPid());
});
// Cancel button
BottomNavigationItemView cancelButton = view.findViewById(R.id.cancel_setup);
// unselect the cancel button because it's selected by default
cancelButton.setSelected(false);
cancelButton.setOnClickListener(v -> {
Timber.i("Cancel button clicked");
// close the app
finish();
});
}
@Override
public Resources.Theme getTheme() {
Resources.Theme theme = super.getTheme();
// try cached value
if (cachedTheme != 0) {
theme.applyStyle(cachedTheme, true);
return theme;
}
// Set the theme
SharedPreferences prefs = MainApplication.getSharedPreferences("mmm");
String themePref = prefs.getString("pref_theme", "system");
switch (themePref) {
case "light" -> {
theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet_Light, true);
cachedTheme = R.style.Theme_MagiskModuleManager_Monet_Light;
}
case "dark" -> {
theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet_Dark, true);
cachedTheme = R.style.Theme_MagiskModuleManager_Monet_Dark;
}
case "system" -> {
theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet, true);
cachedTheme = R.style.Theme_MagiskModuleManager_Monet;
}
case "black" -> {
theme.applyStyle(R.style.Theme_MagiskModuleManager_Monet_Black, true);
cachedTheme = R.style.Theme_MagiskModuleManager_Monet_Black;
}
case "transparent_light" -> {
theme.applyStyle(R.style.Theme_MagiskModuleManager_Transparent_Light, true);
cachedTheme = R.style.Theme_MagiskModuleManager_Transparent_Light;
}
}
return theme;
}
@Override
@SuppressLint({"InlinedApi", "RestrictedApi"})
public void refreshRosettaX() {
// refresh app language
runOnUiThread(() -> {
// refresh activity
Intent intent = new Intent(this, SetupActivity.class);
finish();
startActivity(intent);
});
}
// creates the realm database
private void createRealmDatabase() {
if (realmDatabasesCreated) {
Timber.d("Realm databases already created");
return;
}
Timber.d("Creating Realm databases");
long startTime = System.currentTimeMillis();
// create encryption key
Timber.d("Creating encryption key");
byte[] key = MainApplication.getINSTANCE().getKey();
// create the realm database for ReposList
// create the realm configuration
RealmConfiguration config = new RealmConfiguration.Builder().name("ReposList.realm").directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).encryptionKey(key).build();
// get the instance
Realm.getInstanceAsync(config, new Realm.Callback() {
@Override
public void onSuccess(@NonNull Realm realm) {
Timber.d("Realm instance: %s", realm);
realm.beginTransaction();
// create the ReposList realm database
Timber.d("Creating ReposList realm database");
if (realm.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst() == null) {
Timber.d("Creating androidacy_repo");
// create the androidacy_repo row
// cant use createObject because it crashes because reasons. use copyToRealm instead
ReposList androidacy_repo = realm.createObject(ReposList.class, "androidacy_repo");
Timber.d("Created androidacy_repo object");
androidacy_repo.setName("Androidacy Repo");
Timber.d("Set androidacy_repo name");
androidacy_repo.setDonate("https://www.androidacy.com/membership-account/membership-join/?utm_source=fox-app&utm_medium=app&utm_campaign=app");
Timber.d("Set androidacy_repo donate");
androidacy_repo.setSupport("https://t.me/androidacy_discussions");
Timber.d("Set androidacy_repo support");
androidacy_repo.setSubmitModule("https://www.androidacy.com/module-repository-applications/?utm_source=fox-app&utm_medium=app&utm_campaign=app");
Timber.d("Set androidacy_repo submit module");
androidacy_repo.setUrl(RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT);
Timber.d("Set androidacy_repo url");
androidacy_repo.setEnabled(true);
Timber.d("Set androidacy_repo enabled");
androidacy_repo.setLastUpdate(0);
Timber.d("Set androidacy_repo last update");
androidacy_repo.setWebsite(RepoManager.ANDROIDACY_MAGISK_REPO_HOMEPAGE);
Timber.d("Set androidacy_repo website");
// now copy the data from the data class to the realm object using copyToRealmOrUpdate
Timber.d("Copying data to realm object");
realm.copyToRealmOrUpdate(androidacy_repo);
Timber.d("Created androidacy_repo");
}
// create magisk_alt_repo
if (realm.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst() == null) {
Timber.d("Creating magisk_alt_repo");
ReposList magisk_alt_repo = realm.createObject(ReposList.class, "magisk_alt_repo");
Timber.d("Created magisk_alt_repo object");
magisk_alt_repo.setName("Magisk Alt Repo");
magisk_alt_repo.setDonate(null);
magisk_alt_repo.setWebsite(RepoManager.MAGISK_ALT_REPO_HOMEPAGE);
magisk_alt_repo.setSupport(null);
magisk_alt_repo.setEnabled(true);
magisk_alt_repo.setUrl(RepoManager.MAGISK_ALT_REPO);
magisk_alt_repo.setSubmitModule(RepoManager.MAGISK_ALT_REPO_HOMEPAGE + "/submission");
magisk_alt_repo.setLastUpdate(0);
// commit the changes
Timber.d("Copying data to realm object");
realm.copyToRealmOrUpdate(magisk_alt_repo);
Timber.d("Created magisk_alt_repo");
}
realm.commitTransaction();
realm.close();
realmDatabasesCreated = true;
Timber.d("Realm transaction finished");
long endTime = System.currentTimeMillis();
Timber.d("Realm databases created in %d ms", endTime - startTime);
}
});
}
public void createFiles() {
// use cookiemanager to create the cookie database
try {
CookieManager.getInstance();
} catch (Exception e) {
Timber.e(e);
// show a toast
runOnUiThread(() -> Toast.makeText(this, R.string.error_creating_cookie_database, Toast.LENGTH_LONG).show());
}
// we literally only use these to create the http cache folders
try {
FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/cronet"));
FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/wasm"));
FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/js"));
FileUtils.forceMkdir(new File(MainApplication.getINSTANCE().getDataDir() + "/repos/magisk_alt_repo"));
} catch (IOException e) {
Timber.e(e);
}
createRealmDatabase();
}
@SuppressWarnings("ConstantConditions")
public void disableUpdateActivityForFdroidFlavor() {
if (BuildConfig.FLAVOR.equals("fdroid")) {
// check if the update activity is enabled
PackageManager pm = getPackageManager();
ComponentName componentName = new ComponentName(this, UpdateActivity.class);
int componentEnabledSetting = pm.getComponentEnabledSetting(componentName);
if (componentEnabledSetting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
Timber.d("Disabling update activity for fdroid flavor");
// disable update activity through package manager
pm.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}
}
}
}

@ -0,0 +1,372 @@
package com.fox2code.mmm;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.core.content.FileProvider;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.mmm.utils.io.net.Http;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.android.material.textview.MaterialTextView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.matomo.sdk.extra.TrackHelper;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Objects;
import io.noties.markwon.Markwon;
import timber.log.Timber;
@SuppressWarnings("UnnecessaryReturnStatement")
public class UpdateActivity extends FoxActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (MainApplication.getINSTANCE().isMatomoAllowed) {
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
}
setContentView(R.layout.activity_update);
// Get the progress bar and make it indeterminate for now
LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress);
progressIndicator.setIndeterminate(true);
// get update_cancel button
MaterialButton updateCancel = findViewById(R.id.update_cancel_button);
// get status text view
MaterialTextView statusTextView = findViewById(R.id.update_progress_text);
// set status text to please wait
statusTextView.setText(R.string.please_wait);
Thread updateThread = new Thread() {
public void run() {
// Now, parse the intent
String extras = getIntent().getAction();
// if extras is null, then we are in a bad state or user launched the activity manually
if (extras == null) {
runOnUiThread(() -> {
// set status text to error
statusTextView.setText(R.string.error_no_extras);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
});
return;
}
// get action
ACTIONS action = ACTIONS.valueOf(extras);
// if action is null, then we are in a bad state or user launched the activity manually
if (Objects.isNull(action)) {
runOnUiThread(() -> {
// set status text to error
statusTextView.setText(R.string.error_no_action);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
});
// return
return;
}
// For check action, we need to check if there is an update using the AppUpdateManager.peekShouldUpdate()
if (action == ACTIONS.CHECK) {
checkForUpdate();
} else if (action == ACTIONS.DOWNLOAD) {
try {
downloadUpdate();
} catch (
JSONException e) {
runOnUiThread(() -> {
// set status text to error
statusTextView.setText(R.string.error_download_update);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
});
}
} else if (action == ACTIONS.INSTALL) {
// ensure path was passed and points to a file within our cache directory. replace .. and url encoded characters
String path = getIntent().getStringExtra("path").trim().replaceAll("\\.\\.", "").replaceAll("%2e%2e", "");
if (path.isEmpty()) {
runOnUiThread(() -> {
// set status text to error
statusTextView.setText(R.string.no_file_found);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
});
return;
}
// check and sanitize file path
// path must be in our cache directory
if (!path.startsWith(getCacheDir().getAbsolutePath())) {
throw new SecurityException("Path is not in cache directory: " + path);
}
File file = new File(path);
File parentFile = file.getParentFile();
try {
if (parentFile == null || !parentFile.getCanonicalPath().startsWith(getCacheDir().getCanonicalPath())) {
throw new SecurityException("Path is not in cache directory: " + path);
}
} catch (
IOException e) {
throw new SecurityException("Path is not in cache directory: " + path);
}
if (!file.exists()) {
runOnUiThread(() -> {
// set status text to error
statusTextView.setText(R.string.no_file_found);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
});
// return
return;
}
if (!Objects.equals(file.getParentFile(), getCacheDir())) {
// set status text to error
runOnUiThread(() -> {
statusTextView.setText(R.string.no_file_found);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
});
// return
return;
}
// set status text to installing
statusTextView.setText(R.string.installing_update);
// set progress bar to indeterminate
progressIndicator.setIndeterminate(true);
// install update
installUpdate(file);
}
}
};
// on click, finish the activity and anything running in it
updateCancel.setOnClickListener(v -> {
// end any download
updateThread.interrupt();
forceBackPressed();
finish();
});
updateThread.start();
}
public void checkForUpdate() {
// get status text view
MaterialTextView statusTextView = findViewById(R.id.update_progress_text);
LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress);
runOnUiThread(() -> {
progressIndicator.setIndeterminate(true);
// set status text to checking for update
statusTextView.setText(R.string.checking_for_update);
// set progress bar to indeterminate
progressIndicator.setIndeterminate(true);
});
// check for update
boolean shouldUpdate = AppUpdateManager.getAppUpdateManager().peekShouldUpdate();
// if shouldUpdate is true, then we have an update
if (shouldUpdate) {
runOnUiThread(() -> {
// set status text to update available
statusTextView.setText(R.string.update_available);
// set button text to download
MaterialButton button = findViewById(R.id.update_button);
button.setText(R.string.download_update);
});
// return
} else {
runOnUiThread(() -> {
// set status text to no update available
statusTextView.setText(R.string.no_update_available);
});
// set progress bar to error
// return
}
runOnUiThread(() -> {
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
});
return;
}
public void downloadUpdate() throws JSONException {
LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress);
runOnUiThread(() -> progressIndicator.setIndeterminate(true));
// get status text view
MaterialTextView statusTextView = findViewById(R.id.update_progress_text);
byte[] lastestJSON = new byte[0];
try {
lastestJSON = Http.doHttpGet(AppUpdateManager.RELEASES_API_URL, false);
} catch (
Exception e) {
// when logging, REMOVE the json from the log
Timber.e(e, "Error downloading update info");
runOnUiThread(() -> {
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
statusTextView.setText(R.string.error_download_update);
});
}
// convert to JSON
JSONObject latestJSON = new JSONObject(new String(lastestJSON));
String changelog = latestJSON.getString("body");
// set changelog text. changelog could be markdown, so we need to convert it to HTML
MaterialTextView changelogTextView = findViewById(R.id.update_changelog);
final Markwon markwon = Markwon.builder(this).build();
runOnUiThread(() -> markwon.setMarkdown(changelogTextView, changelog));
// we already know that there is an update, so we can get the latest version of our architecture. We're going to have to iterate through the assets to find the one we want
JSONArray assets = latestJSON.getJSONArray("assets");
// get the asset we want
JSONObject asset = null;
// iterate through assets until we find the one that contains Build.SUPPORTED_ABIS[0]
while (Objects.isNull(asset)) {
for (int i = 0; i < assets.length(); i++) {
JSONObject asset1 = assets.getJSONObject(i);
if (asset1.getString("name").contains(Build.SUPPORTED_ABIS[0])) {
asset = asset1;
break;
}
}
}
// if asset is null, then we are in a bad state
if (Objects.isNull(asset)) {
// set status text to error
runOnUiThread(() -> {
statusTextView.setText(R.string.error_no_asset);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
});
// return
return;
}
// get the download url
String downloadUrl = Objects.requireNonNull(asset).getString("browser_download_url");
// get the download size
long downloadSize = asset.getLong("size");
runOnUiThread(() -> {
// set status text to downloading update
statusTextView.setText(getString(R.string.downloading_update, 0));
// set progress bar to 0
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
});
// download the update
byte[] update = new byte[0];
try {
update = Http.doHttpGet(downloadUrl, (downloaded, total, done) -> runOnUiThread(() -> {
// update progress bar
progressIndicator.setProgressCompat((int) (((float) downloaded / (float) total) * 100), true);
// update status text
statusTextView.setText(getString(R.string.downloading_update, (int) (((float) downloaded / (float) total) * 100)));
}));
} catch (
Exception e) {
runOnUiThread(() -> {
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
statusTextView.setText(R.string.error_download_update);
});
}
// if update is null, then we are in a bad state
if (Objects.isNull(update)) {
runOnUiThread(() -> {
// set status text to error
statusTextView.setText(R.string.error_download_update);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
});
// return
return;
}
// if update is not the same size as the download size, then we are in a bad state
if (update.length != downloadSize) {
runOnUiThread(() -> {
// set status text to error
statusTextView.setText(R.string.error_download_update);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
});
// return
return;
}
// set status text to installing update
runOnUiThread(() -> {
statusTextView.setText(R.string.installing_update);
// set progress bar to 100
progressIndicator.setIndeterminate(true);
progressIndicator.setProgressCompat(100, false);
});
// save the update to the cache
File updateFile = null;
FileOutputStream fileOutputStream = null;
try {
updateFile = new File(getCacheDir(), "update.apk");
fileOutputStream = new FileOutputStream(updateFile);
fileOutputStream.write(update);
} catch (
IOException e) {
runOnUiThread(() -> {
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
statusTextView.setText(R.string.error_download_update);
});
} finally {
if (Objects.nonNull(updateFile)) {
Objects.requireNonNull(updateFile).deleteOnExit();
}
try {
Objects.requireNonNull(fileOutputStream).close();
} catch (
IOException ignored) {
}
}
// install the update
installUpdate(updateFile);
// return
return;
}
@SuppressLint("RestrictedApi")
private void installUpdate(File updateFile) {
// get status text view
runOnUiThread(() -> {
MaterialTextView statusTextView = findViewById(R.id.update_progress_text);
// set status text to installing update
statusTextView.setText(R.string.installing_update);
// set progress bar to 100
LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress);
progressIndicator.setIndeterminate(true);
progressIndicator.setProgressCompat(100, false);
});
// request install permissions
Intent intent = new Intent(Intent.ACTION_VIEW);
Context context = getApplicationContext();
Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".file-provider", updateFile);
intent.setDataAndTypeAndNormalize(uri, "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
// return
return;
}
public enum ACTIONS {
// action can be CHECK, DOWNLOAD, INSTALL
CHECK, DOWNLOAD, INSTALL
}
}

@ -16,8 +16,11 @@ import java.util.Collection;
* Class made to expose some manager functions to xposed modules.
* It will not be obfuscated on release builds
*/
@SuppressWarnings("unused")
@Keep
public class XHooks {
public enum XHooks {
;
@Keep
public static void onRepoManagerInitialize() {
// Call addXRepo here if you are an XPosed module

@ -5,11 +5,12 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.net.http.SslError;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.webkit.ConsoleMessage;
import android.webkit.CookieManager;
import android.webkit.SslErrorHandler;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
@ -22,35 +23,38 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.webkit.WebResourceErrorCompat;
import androidx.webkit.WebSettingsCompat;
import androidx.webkit.WebViewClientCompat;
import androidx.webkit.WebViewFeature;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.XHooks;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.io.net.Http;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import org.json.JSONException;
import org.json.JSONObject;
import org.matomo.sdk.extra.TrackHelper;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import timber.log.Timber;
/**
* Per Androidacy repo implementation agreement, no request of this WebView shall be modified.
*/
public final class AndroidacyActivity extends FoxActivity {
private static final String TAG = "AndroidacyActivity";
static {
if (BuildConfig.DEBUG) {
@ -68,29 +72,32 @@ public final class AndroidacyActivity extends FoxActivity {
@SuppressWarnings("deprecation")
@Override
@SuppressLint({"SetJavaScriptEnabled", "JavascriptInterface", "RestrictedApi"})
@SuppressLint({"SetJavaScriptEnabled", "JavascriptInterface", "RestrictedApi", "ClickableViewAccessibility"})
protected void onCreate(@Nullable Bundle savedInstanceState) {
this.moduleFile = new File(this.getCacheDir(), "module.zip");
super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
Intent intent = this.getIntent();
Uri uri;
if (!MainApplication.checkSecret(intent) ||
(uri = intent.getData()) == null) {
Log.w(TAG, "Impersonation detected");
if (!MainApplication.checkSecret(intent) || (uri = intent.getData()) == null) {
Timber.w("Impersonation detected");
this.forceBackPressed();
return;
}
String url = uri.toString();
if (!AndroidacyUtil.isAndroidacyLink(url, uri)) {
Log.w(TAG, "Calling non androidacy link in secure WebView: " + url);
Timber.w("Calling non androidacy link in secure WebView: %s", url);
this.forceBackPressed();
return;
}
if (!Http.hasWebView()) {
Log.w(TAG, "No WebView found to load url: " + url);
Timber.w("No WebView found to load url: %s", url);
this.forceBackPressed();
return;
}
// if action bar is shown, hide it
this.hideActionBar();
Http.markCaptchaAndroidacySolved();
if (!url.contains(AndroidacyUtil.REFERRER)) {
if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
url = url + '&' + AndroidacyUtil.REFERRER;
@ -98,8 +105,27 @@ public final class AndroidacyActivity extends FoxActivity {
url = url + '?' + AndroidacyUtil.REFERRER;
}
}
boolean allowInstall = intent.getBooleanExtra(
Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, false);
// Add token to url if not present
String token = uri.getQueryParameter("token");
if (token == null) {
// get from shared preferences
url = url + "&token=" + AndroidacyRepoData.token;
}
// Add device_id to url if not present
String device_id = uri.getQueryParameter("device_id");
if (device_id == null) {
// get from shared preferences
device_id = AndroidacyRepoData.generateDeviceId();
url = url + "&device_id=" + device_id;
}
// check if client_id is present
String client_id = uri.getQueryParameter("client_id");
if (client_id == null) {
// get from shared preferences
client_id = BuildConfig.ANDROIDACY_CLIENT_ID;
url = url + "&client_id=" + client_id;
}
boolean allowInstall = intent.getBooleanExtra(Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, false);
String title = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_TITLE);
String config = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG);
int compatLevel = intent.getIntExtra(Constants.EXTRA_ANDROIDACY_COMPAT_LEVEL, 0);
@ -118,11 +144,10 @@ public final class AndroidacyActivity extends FoxActivity {
String configPkg = IntentHelper.getPackageOfConfig(config);
try {
XHooks.checkConfigTargetExists(this, configPkg, config);
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24,
menu -> {
IntentHelper.openConfig(this, config);
return true;
});
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, menu -> {
IntentHelper.openConfig(this, config);
return true;
});
} catch (PackageManager.NameNotFoundException ignored) {
}
}
@ -130,45 +155,56 @@ public final class AndroidacyActivity extends FoxActivity {
this.progressIndicator = this.findViewById(R.id.progress_bar);
this.progressIndicator.setMax(100);
this.webView = this.findViewById(R.id.webView);
this.webViewNote = this.findViewById(R.id.webViewNote);
WebSettings webSettings = this.webView.getSettings();
webSettings.setUserAgentString(Http.getAndroidacyUA());
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.setAcceptThirdPartyCookies(this.webView, true);
webSettings.setDomStorageEnabled(true);
webSettings.setJavaScriptEnabled(true);
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setAllowFileAccess(false);
// Attempt at fixing CloudFlare captcha.
if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_CONTROL)) {
WebSettingsCompat.setRequestedWithHeaderMode(
webSettings, WebSettingsCompat.REQUESTED_WITH_HEADER_MODE_NO_HEADER);
webSettings.setAllowContentAccess(false);
webSettings.setAllowFileAccessFromFileURLs(false);
webSettings.setAllowUniversalAccessFromFileURLs(false);
webSettings.setMediaPlaybackRequiresUserGesture(false);
// enable webview debugging on debug builds
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true);
}
// If API level is .= 33, allow setAlgorithmicDarkeningAllowed
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
try {
webSettings.setAlgorithmicDarkeningAllowed(true);
} catch (NoSuchMethodError ignored) {
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Make website follow app theme
webSettings.setForceDark(MainApplication.getINSTANCE().isLightTheme() ?
WebSettings.FORCE_DARK_OFF : WebSettings.FORCE_DARK_ON);
// if app is in dark mode, force dark mode on webview
if (MainApplication.getINSTANCE().isDarkTheme()) {
// for api 33, use setAlgorithmicDarkeningAllowed, for api 29-32 use setForceDark, for api 28 and below use setForceDarkStrategy
if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
WebSettingsCompat.setAlgorithmicDarkeningAllowed(webSettings, true);
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
// If api level is < 32, use force dark
WebSettingsCompat.setForceDark(webSettings, MainApplication.getINSTANCE().isLightTheme() ?
WebSettingsCompat.FORCE_DARK_OFF : WebSettingsCompat.FORCE_DARK_ON);
WebSettingsCompat.setForceDark(webSettings, WebSettingsCompat.FORCE_DARK_ON);
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
WebSettingsCompat.setForceDarkStrategy(webSettings, WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY);
}
}
// Attempt at fixing CloudFlare captcha.
if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) {
Set<String> allowList = new HashSet<>();
allowList.add("https://*.androidacy.com");
WebSettingsCompat.setRequestedWithHeaderOriginAllowList(webSettings, allowList);
}
// get swipe to refresh layout
SwipeRefreshLayout swipeRefreshLayout = this.findViewById(R.id.swipe_refresh_layout);
this.webView.setWebViewClient(new WebViewClientCompat() {
private String pageUrl;
@Override
public boolean shouldOverrideUrlLoading(
@NonNull WebView view, @NonNull WebResourceRequest request) {
public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull WebResourceRequest request) {
// Don't open non Androidacy urls inside WebView
if (request.isForMainFrame() &&
!AndroidacyUtil.isAndroidacyLink(request.getUrl())) {
if (request.isForMainFrame() && !AndroidacyUtil.isAndroidacyLink(request.getUrl())) {
if (downloadMode || backOnResume) return true;
Log.i(TAG, "Exiting WebView " + // hideToken in case isAndroidacyLink fail.
AndroidacyUtil.hideToken(request.getUrl().toString()));
// sanitize url
String url = request.getUrl().toString();
//noinspection UnnecessaryCallToStringValueOf
url = String.valueOf(AndroidacyUtil.hideToken(url));
Timber.i("Exiting WebView %s", url);
IntentHelper.openUri(view.getContext(), request.getUrl().toString());
return true;
}
@ -177,13 +213,10 @@ public final class AndroidacyActivity extends FoxActivity {
@Nullable
@Override
public WebResourceResponse shouldInterceptRequest(
WebView view, WebResourceRequest request) {
if (AndroidacyActivity.this.megaIntercept(
this.pageUrl, request.getUrl().toString())) {
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
if (AndroidacyActivity.this.megaIntercept(this.pageUrl, request.getUrl().toString())) {
// Block request as Androidacy doesn't allow duplicate requests
return new WebResourceResponse("text/plain", "UTF-8",
new ByteArrayInputStream(new byte[0]));
return new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream(new byte[0]));
}
return null;
}
@ -195,21 +228,16 @@ public final class AndroidacyActivity extends FoxActivity {
@Override
public void onPageFinished(WebView view, String url) {
webViewNote.setVisibility(View.GONE);
progressIndicator.setVisibility(View.INVISIBLE);
progressIndicator.setProgressCompat(0, false);
}
private void onReceivedError(String url, int errorCode) {
if ((url.startsWith("https://production-api.androidacy.com/magisk/") ||
url.startsWith("https://staging-api.androidacy.com/magisk/") ||
url.equals(pageUrl)) && (errorCode == 419 || errorCode == 429 || errorCode == 503)) {
Toast.makeText(AndroidacyActivity.this,
"Too many requests!", Toast.LENGTH_LONG).show();
if ((url.startsWith("https://production-api.androidacy.com/magisk/") || url.startsWith("https://staging-api.androidacy.com/magisk/") || url.equals(pageUrl)) && (errorCode == 419 || errorCode == 429 || errorCode == 503)) {
Toast.makeText(AndroidacyActivity.this, "Too many requests!", Toast.LENGTH_LONG).show();
AndroidacyActivity.this.runOnUiThread(AndroidacyActivity.this::onBackPressed);
} else if (url.equals(this.pageUrl)) {
postOnUiThread(() ->
webViewNote.setVisibility(View.VISIBLE));
postOnUiThread(() -> webViewNote.setVisibility(View.VISIBLE));
}
}
@ -219,116 +247,89 @@ public final class AndroidacyActivity extends FoxActivity {
}
@Override
public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request,
@NonNull WebResourceErrorCompat error) {
public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request, @NonNull WebResourceErrorCompat error) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) {
this.onReceivedError(request.getUrl().toString(), error.getErrorCode());
}
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
super.onReceivedSslError(view, handler, error);
// log the error and url of its request
Timber.tag("JSLog").e(error.toString());
}
});
// logic for swipe to refresh
swipeRefreshLayout.setOnRefreshListener(() -> {
swipeRefreshLayout.setRefreshing(false);
// reload page
webView.reload();
});
this.webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams) {
FoxActivity.getFoxActivity(webView).startActivityForResult(
fileChooserParams.createIntent(), (code, data) ->
filePathCallback.onReceiveValue(
FileChooserParams.parseResult(code, data)));
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
FoxActivity.getFoxActivity(webView).startActivityForResult(fileChooserParams.createIntent(), (code, data) -> filePathCallback.onReceiveValue(FileChooserParams.parseResult(code, data)));
return true;
}
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
if (BuildConfig.DEBUG) {
switch (consoleMessage.messageLevel()) {
case TIP:
Log.v(TAG, consoleMessage.message());
break;
case LOG:
Log.i(TAG, consoleMessage.message());
break;
case WARNING:
Log.w(TAG, consoleMessage.message());
break;
case ERROR:
Log.e(TAG, consoleMessage.message());
break;
case DEBUG:
Log.d(TAG, consoleMessage.message());
break;
}
switch (consoleMessage.messageLevel()) {
case TIP -> Timber.tag("JSLog").i(consoleMessage.message());
case LOG -> Timber.tag("JSLog").d(consoleMessage.message());
case WARNING -> Timber.tag("JSLog").w(consoleMessage.message());
case ERROR -> Timber.tag("JSLog").e(consoleMessage.message());
default -> Timber.tag("JSLog").v(consoleMessage.message());
}
return super.onConsoleMessage(consoleMessage);
return true;
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (downloadMode) return;
if (newProgress != 100 && // Show progress bar
progressIndicator.getVisibility() != View.VISIBLE)
if (newProgress != 100 && progressIndicator.getVisibility() != View.VISIBLE) {
Timber.i("Progress: %d, showing progress bar", newProgress);
progressIndicator.setVisibility(View.VISIBLE);
}
// if progress is greater than one, set indeterminate to false
if (newProgress > 1) {
Timber.i("Progress: %d, setting indeterminate to false", newProgress);
progressIndicator.setIndeterminate(false);
}
progressIndicator.setProgressCompat(newProgress, true);
if (newProgress == 100 && // Hide progress bar
progressIndicator.getVisibility() != View.INVISIBLE)
progressIndicator.setVisibility(View.INVISIBLE);
if (newProgress == 100 && progressIndicator.getVisibility() != View.INVISIBLE) {
Timber.i("Progress: %d, hiding progress bar", newProgress);
progressIndicator.setIndeterminate(true);
progressIndicator.setVisibility(View.GONE);
}
}
});
this.webView.setDownloadListener((
downloadUrl, userAgent, contentDisposition, mimetype, contentLength) -> {
this.webView.setDownloadListener((downloadUrl, userAgent, contentDisposition, mimetype, contentLength) -> {
if (this.downloadMode || this.isDownloadUrl(downloadUrl)) return;
if (AndroidacyUtil.isAndroidacyLink(downloadUrl) && !this.backOnResume) {
AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI;
if (androidacyWebAPI != null) {
if (!androidacyWebAPI.downloadMode) {
// Native module popup may cause download after consumed action
if (androidacyWebAPI.consumedAction)
return;
if (androidacyWebAPI.consumedAction) return;
// Workaround Androidacy bug
final String moduleId = moduleIdOfUrl(downloadUrl);
if (moduleId != null && !this.isFileUrl(downloadUrl)) {
webView.evaluateJavascript("document.querySelector(" +
"\"#download-form input[name=_token]\").value",
result -> new Thread("Androidacy popup workaround thread") {
@Override
public void run() {
if (androidacyWebAPI.consumedAction) return;
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("moduleId", moduleId);
jsonObject.put("token", AndroidacyRepoData
.getInstance().getToken());
jsonObject.put("_token", result);
String realUrl = Http.doHttpPostRedirect(downloadUrl,
jsonObject.toString(), true);
if (downloadUrl.equals(realUrl)) {
Log.e(TAG, "Failed to resolve URL from " +
downloadUrl);
AndroidacyActivity.this.megaIntercept(
webView.getUrl(), downloadUrl);
return;
}
Log.i(TAG, "Got url: " + realUrl);
androidacyWebAPI.openNativeModuleDialogRaw(realUrl,
moduleId, "", androidacyWebAPI.canInstall());
} catch (IOException | JSONException e) {
Log.e(TAG, "Failed redirect intercept", e);
}
}
}.start());
return;
} else if (this.megaIntercept(webView.getUrl(), downloadUrl))
if (this.megaIntercept(webView.getUrl(), downloadUrl)) {
// Block request as Androidacy doesn't allow duplicate requests
return;
} else if (moduleId != null) {
// Download module
Timber.i("megaIntercept failure. Forcing onBackPress");
this.onBackPressed();
}
}
androidacyWebAPI.consumedAction = true;
androidacyWebAPI.downloadMode = false;
}
this.backOnResume = true;
Log.i(TAG, "Exiting WebView " +
AndroidacyUtil.hideToken(downloadUrl));
for (String prefix : new String[]{
"https://production-api.androidacy.com/magisk/download/",
"https://staging-api.androidacy.com/magisk/download/"
}) {
Timber.i("Exiting WebView %s", AndroidacyUtil.hideToken(downloadUrl));
for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/magisk/downloads/"}) {
if (downloadUrl.startsWith(prefix)) {
return;
}
@ -341,21 +342,11 @@ public final class AndroidacyActivity extends FoxActivity {
this.webView.addJavascriptInterface(this.androidacyWebAPI, "mmm");
if (compatLevel != 0) androidacyWebAPI.notifyCompatModeRaw(compatLevel);
HashMap<String, String> headers = new HashMap<>();
headers.put("Accept-Language", this.getResources()
.getConfiguration().locale.toLanguageTag());
headers.put("Accept-Language", this.getResources().getConfiguration().locale.toLanguageTag());
// set layout to view
this.webView.loadUrl(url, headers);
}
@Override
public void onBackPressed() {
WebView webView = this.webView;
if (webView != null && webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
}
}
@Override
protected void onResume() {
super.onResume();
@ -368,14 +359,7 @@ public final class AndroidacyActivity extends FoxActivity {
}
private String moduleIdOfUrl(String url) {
for (String prefix : new String[]{
"https://production-api.androidacy.com/magisk/download/",
"https://staging-api.androidacy.com/magisk/download/",
"https://production-api.androidacy.com/magisk/readme/",
"https://staging-api.androidacy.com/magisk/readme/",
"https://prodiuction-api.androidacy.com/magisk/info/",
"https://staging-api.androidacy.com/magisk/info/"
}) { // Make both staging and non staging act the same
for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/downloads/", "https://production-api.androidacy.com/magisk/readme/", "https://staging-api.androidacy.com/magisk/readme/", "https://prodiuction-api.androidacy.com/magisk/info/", "https://staging-api.androidacy.com/magisk/info/"}) { // Make both staging and non staging act the same
int i = url.indexOf('?', prefix.length());
if (i == -1) i = url.length();
if (url.startsWith(prefix)) return url.substring(prefix.length(), i);
@ -396,20 +380,14 @@ public final class AndroidacyActivity extends FoxActivity {
private boolean isFileUrl(String url) {
if (url == null) return false;
for (String prefix : new String[]{
"https://production-api.androidacy.com/magisk/file/",
"https://staging-api.androidacy.com/magisk/file/"
}) { // Make both staging and non staging act the same
for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/downloads/"}) { // Make both staging and non staging act the same
if (url.startsWith(prefix)) return true;
}
return false;
}
private boolean isDownloadUrl(String url) {
for (String prefix : new String[]{
"https://production-api.androidacy.com/magisk/download/",
"https://staging-api.androidacy.com/magisk/download/"
}) { // Make both staging and non staging act the same
for (String prefix : new String[]{"https://production-api.androidacy.com/magisk/downloads/", "https://staging-api.androidacy.com/magisk/downloads/"}) { // Make both staging and non staging act the same
if (url.startsWith(prefix)) return true;
}
return false;
@ -417,20 +395,21 @@ public final class AndroidacyActivity extends FoxActivity {
private boolean megaIntercept(String pageUrl, String fileUrl) {
if (pageUrl == null || fileUrl == null) return false;
if (this.isFileUrl(fileUrl)) {
Log.d(TAG, "megaIntercept(" +
AndroidacyUtil.hideToken(pageUrl) + ", " +
AndroidacyUtil.hideToken(fileUrl) + ")");
} else return false;
// ensure neither pageUrl nor fileUrl are going to cause a crash
if (pageUrl.contains(" ") || fileUrl.contains(" ")) return false;
if (!this.isFileUrl(fileUrl)) {
return false;
}
final AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI;
String moduleId = this.moduleIdOfUrl(fileUrl);
if (moduleId == null) moduleId = this.moduleIdOfUrl(pageUrl);
String moduleId = AndroidacyUtil.getModuleId(fileUrl);
if (moduleId == null) {
Log.d(TAG, "No module id?");
return false;
Timber.i("No module id?");
// Re-open the page
this.webView.loadUrl(pageUrl + "&force_refresh=" + System.currentTimeMillis());
}
androidacyWebAPI.openNativeModuleDialogRaw(fileUrl,
moduleId, "", androidacyWebAPI.canInstall());
String checksum = AndroidacyUtil.getChecksumFromURL(fileUrl);
String moduleTitle = AndroidacyUtil.getModuleTitle(fileUrl);
androidacyWebAPI.openNativeModuleDialogRaw(fileUrl, moduleId, moduleTitle, checksum, androidacyWebAPI.canInstall());
return true;
}
@ -442,20 +421,29 @@ public final class AndroidacyActivity extends FoxActivity {
});
byte[] module;
try {
module = Http.doHttpGet(url, (downloaded, total, done) ->
progressIndicator.setProgressCompat((downloaded * 100) / total, true));
module = Http.doHttpGet(url, (downloaded, total, done) -> progressIndicator.setProgressCompat((downloaded * 100) / total, true));
try (FileOutputStream fileOutputStream = new FileOutputStream(this.moduleFile)) {
fileOutputStream.write(module);
}
} finally {
//noinspection UnusedAssignment
module = null;
this.runOnUiThread(() ->
progressIndicator.setVisibility(View.INVISIBLE));
this.runOnUiThread(() -> progressIndicator.setVisibility(View.INVISIBLE));
}
this.backOnResume = true;
this.downloadMode = false;
return FileProvider.getUriForFile(this,
this.getPackageName() + ".file-provider",
this.moduleFile);
return FileProvider.getUriForFile(this, this.getPackageName() + ".file-provider", this.moduleFile);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (webView != null) {
SwipeRefreshLayout parent = (SwipeRefreshLayout) webView.getParent();
parent.removeView(webView);
webView.removeAllViews();
webView.destroy(); // fix memory leak
}
Timber.i("onDestroy for %s", this);
}
}

@ -1,63 +1,73 @@
package com.fox2code.mmm.androidacy;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
import android.webkit.CookieManager;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.repo.RepoData;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.io.net.HttpException;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.Shell;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import okhttp3.HttpUrl;
import timber.log.Timber;
@SuppressWarnings("KotlinInternalInJava")
public final class AndroidacyRepoData extends RepoData {
private static final String TAG = "AndroidacyRepoData";
public static String ANDROIDACY_DEVICE_ID = null;
public static String token = MainApplication.getSharedPreferences("androidacy").getString("pref_androidacy_api_token", null);
static {
HttpUrl.Builder OK_HTTP_URL_BUILDER =
new HttpUrl.Builder().scheme("https");
HttpUrl.Builder OK_HTTP_URL_BUILDER = new HttpUrl.Builder().scheme("https");
// Using HttpUrl.Builder.host(String) crash the app
OK_HTTP_URL_BUILDER.setHost$okhttp(".androidacy.com");
OK_HTTP_URL_BUILDER.build();
}
// Avoid spamming requests to Androidacy
private long androidacyBlockade = 0;
private String token = null;
@SuppressWarnings("unused")
public final String ClientID = BuildConfig.ANDROIDACY_CLIENT_ID;
private final boolean testMode;
private final String host;
public String[][] userInfo = new String[][]{{"role", null}, {"permissions", null}};
public String memberLevel;
// Avoid spamming requests to Androidacy
private long androidacyBlockade = 0;
public AndroidacyRepoData(File cacheRoot, SharedPreferences cachedPreferences,
boolean testMode) {
super(testMode ? RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT :
RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT, cacheRoot, cachedPreferences);
if (this.metaDataCache.exists() && !testMode) {
this.androidacyBlockade = this.metaDataCache.lastModified() + 30_000L;
if (this.androidacyBlockade - 60_000L > System.currentTimeMillis()) {
this.androidacyBlockade = 0; // Don't allow time travel. Well why not???
}
}
public AndroidacyRepoData(File cacheRoot, boolean testMode) {
super(testMode ? RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT : RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT, cacheRoot);
this.defaultName = "Androidacy Modules Repo";
this.defaultWebsite = RepoManager.ANDROIDACY_MAGISK_REPO_HOMEPAGE;
this.defaultSupport = "https://t.me/androidacy_discussions";
this.defaultDonate = "https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm-medium=app&utm_campaign=fox-inapp";
this.defaultDonate = "https://www.androidacy.com/membership-account/membership-checkout/?level=2&discount_code=FOX2CODE&utm_souce=foxmmm&utm_medium=android-app&utm_campaign=fox-upgrade-promo";
this.defaultSubmitModule = "https://www.androidacy.com/module-repository-applications/";
this.host = testMode ? "staging-api.androidacy.com" : "production-api.androidacy.com";
this.testMode = testMode;
@ -67,82 +77,242 @@ public final class AndroidacyRepoData extends RepoData {
return RepoManager.getINSTANCE().getAndroidacyRepoData();
}
public <string> boolean isValidToken(string token) {
private static String filterURL(String url) {
if (url == null || url.isEmpty() || PropUtils.isInvalidURL(url)) {
return null;
}
return url;
}
// Generates a unique device ID. This is used to identify the device in the API for rate
// limiting and fraud detection.
public static String generateDeviceId() {
// first, check if ANDROIDACY_DEVICE_ID is already set
if (ANDROIDACY_DEVICE_ID != null) {
return ANDROIDACY_DEVICE_ID;
}
// Try to get the device ID from the shared preferences
SharedPreferences sharedPreferences = MainApplication.getSharedPreferences("androidacy");
String deviceIdPref = sharedPreferences.getString("device_id", null);
if (deviceIdPref != null) {
ANDROIDACY_DEVICE_ID = deviceIdPref;
return deviceIdPref;
} else {
// Really not that scary - just hashes some device info. We can't even get the info
// we originally hashed, so it's not like we can use it to track you.
String deviceId = null;
// Get ro.serialno if it exists
// First, we need to get an su shell
Shell.Result result = Shell.cmd("getprop ro.serialno").exec();
// Check if the command was successful
if (result.isSuccess()) {
// Get the output
String output = result.getOut().get(0);
// Check if the output is valid
if (output != null && !output.isEmpty()) {
deviceId = output;
}
}
// Now, get device model, manufacturer, and Android version originally from
String deviceModel = android.os.Build.MODEL;
String deviceManufacturer = android.os.Build.MANUFACTURER;
String androidVersion = android.os.Build.VERSION.RELEASE;
// Append it all together
deviceId += deviceModel + deviceManufacturer + androidVersion;
// Hash it
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException ignored) {
// This should never happen so we can just return the original device ID
ANDROIDACY_DEVICE_ID = deviceId;
return deviceId;
}
byte[] hash = digest.digest(deviceId.getBytes());
// Convert it to a hex string
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
// Save it to shared preferences
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("device_id", hexString.toString());
editor.apply();
// Set ANDROIDACY_DEVICE_ID
ANDROIDACY_DEVICE_ID = hexString.toString();
// Return it
return hexString.toString();
}
}
public boolean isValidToken(String token) throws IOException {
String deviceId = generateDeviceId();
try {
Http.doHttpGet("https://" + this.host + "/auth/me?token=" + token, false);
} catch (Exception e) {
if ("Received error code: 419".equals(e.getMessage()) ||
"Received error code: 429".equals(e.getMessage())) {
Log.e(TAG, "We are being rate limited!", e);
long time = System.currentTimeMillis();
this.androidacyBlockade = time + 3_600_000L;
byte[] resp = Http.doHttpGet("https://" + this.host + "/auth/me?token=" + token + "&device_id=" + deviceId + "&client_id=" + BuildConfig.ANDROIDACY_CLIENT_ID, false);
// response is JSON
JSONObject jsonObject = new JSONObject(new String(resp));
memberLevel = jsonObject.getString("role");
JSONArray memberPermissions = jsonObject.getJSONArray("permissions");
// set role and permissions on userInfo property
userInfo = new String[][]{{"role", memberLevel}, {"permissions", String.valueOf(memberPermissions)}};
return true;
} catch (HttpException e) {
if (e.getErrorCode() == 401) {
Timber.w("Invalid token, resetting...");
// Remove saved preference
SharedPreferences.Editor editor = MainApplication.getSharedPreferences("androidacy").edit();
editor.remove("pref_androidacy_api_token");
editor.apply();
return false;
}
Log.w(TAG, "Invalid token, resetting...");
throw e;
} catch (JSONException e) {
// response is not JSON
Timber.w("Invalid token, resetting...");
Timber.w(e);
// Remove saved preference
SharedPreferences.Editor editor = this.cachedPreferences.edit();
editor.remove("androidacy_api_token");
SharedPreferences.Editor editor = MainApplication.getSharedPreferences("androidacy").edit();
editor.remove("pref_androidacy_api_token");
editor.apply();
return false;
}
// If status code is 200, we are good
return true;
}
@SuppressLint({"RestrictedApi", "BinaryOperationInTimber"})
@Override
protected boolean prepare() {
// If ANDROIDACY_CLIENT_ID is not set or is empty, disable this repo and return
if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) {
SharedPreferences.Editor editor = MainApplication.getSharedPreferences("mmm").edit();
editor.putBoolean("pref_androidacy_repo_enabled", false);
editor.apply();
Timber.w("ANDROIDACY_CLIENT_ID is empty, disabling AndroidacyRepoData 2");
return false;
}
if (Http.needCaptchaAndroidacy()) return false;
// Implementation details discussed on telegram
// DEPRECATED. Please switch to new implementation before v7 hits production
// First, ping the server to check if it's alive
try {
HttpURLConnection connection = (HttpURLConnection) new URL("https://" + this.host + "/ping").openConnection();
connection.setRequestMethod("GET");
connection.setReadTimeout(5000);
connection.connect();
if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) {
// If it's a 400, the app is probably outdated. Show a snackbar suggesting user update app and webview
if (connection.getResponseCode() == 400) {
// Show a dialog using androidacy_update_needed string
new MaterialAlertDialogBuilder(MainApplication.getINSTANCE()).setTitle(R.string.androidacy_update_needed).setMessage(R.string.androidacy_update_needed_message).setPositiveButton(R.string.update, (dialog, which) -> {
// Open the app's page on the Play Store
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.androidacy.com/downloads/?view=FoxMMM&utm_source=foxmnm&utm_medium=app&utm_campaign=android-app"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
MainApplication.getINSTANCE().startActivity(intent);
}).setNegativeButton(R.string.cancel, null).show();
}
return false;
}
} catch (Exception e) {
Timber.e(e, "Failed to ping server");
return false;
}
String deviceId = generateDeviceId();
long time = System.currentTimeMillis();
if (this.androidacyBlockade > time) return false;
if (this.androidacyBlockade > time) return true; // fake it till you make it. Basically,
// don't fail just because we're rate limited. API and web rate limits are different.
this.androidacyBlockade = time + 30_000L;
// Get token from androidacy_api_token shared preference
String token = this.cachedPreferences.getString("androidacy_api_token", null);
if (token != null) {
this.token = token;
if (!isValidToken(token)) {
try {
if (token == null) {
token = MainApplication.getSharedPreferences("androidacy").getString("pref_androidacy_api_token", null);
if (token != null && !this.isValidToken(token)) {
Timber.i("Token expired or invalid, requesting new one...");
token = null;
} else {
Timber.i("Using cached token");
}
} else if (!this.isValidToken(token)) {
Timber.i("Token expired, requesting new one...");
token = null;
} else {
Timber.i("Using validated cached token");
}
} catch (IOException e) {
if (HttpException.shouldTimeout(e)) {
Timber.e(e, "We are being rate limited!");
this.androidacyBlockade = time + 3_600_000L;
}
return false;
}
if (token == null) {
Timber.i("Token is null, requesting new one...");
try {
Log.i(TAG, "Refreshing token...");
// POST request to https://production-api.androidacy.com/auth/register
token = new String(Http.doHttpPost(
"https://" + this.host + "/auth/register",
"foxmmm=true", true), StandardCharsets.UTF_8);
Timber.i("Requesting new token...");
// POST json request to https://production-api.androidacy.com/auth/register
token = new String(Http.doHttpPost("https://" + this.host + "/auth/register?client_id=" + BuildConfig.ANDROIDACY_CLIENT_ID, "{\"device_id\":\"" + deviceId + "\"}", false));
// Parse token
JSONObject jsonObject = new JSONObject(token);
token = jsonObject.getString("token");
// Save token to shared preference
SharedPreferences.Editor editor = this.cachedPreferences.edit();
editor.putString("androidacy_api_token", token);
editor.apply();
try {
JSONObject jsonObject = new JSONObject(token);
// log last four of token, replacing the rest with asterisks
token = jsonObject.getString("token");
//noinspection SuspiciousRegexArgument
Timber.d("Token: %s", token.substring(0, token.length() - 4).replaceAll(".", "*") + token.substring(token.length() - 4));
memberLevel = jsonObject.getString("role");
} catch (JSONException e) {
Timber.e(e, "Failed to parse token");
// Show a toast
Looper mainLooper = Looper.getMainLooper();
Handler handler = new Handler(mainLooper);
handler.post(() -> Toast.makeText(MainApplication.getINSTANCE(), R.string.androidacy_failed_to_parse_token, Toast.LENGTH_LONG).show());
return false;
}
// Ensure token is valid
if (!isValidToken(token)) {
Timber.e("Failed to validate token");
// Show a toast
Looper mainLooper = Looper.getMainLooper();
Handler handler = new Handler(mainLooper);
handler.post(() -> Toast.makeText(MainApplication.getINSTANCE(), R.string.androidacy_failed_to_validate_token, Toast.LENGTH_LONG).show());
return false;
} else {
// Save token to shared preference
SharedPreferences.Editor editor = MainApplication.getSharedPreferences("androidacy").edit();
editor.putString("pref_androidacy_api_token", token);
editor.apply();
Timber.i("Token saved to shared preference");
}
} catch (Exception e) {
if ("Received error code: 419".equals(e.getMessage()) ||
"Received error code: 429".equals(e.getMessage()) ||
"Received error code: 503".equals(e.getMessage())
) {
Log.e(TAG, "We are being rate limited!", e);
if (HttpException.shouldTimeout(e)) {
Timber.e(e, "We are being rate limited!");
this.androidacyBlockade = time + 3_600_000L;
}
Log.e(TAG, "Failed to get a new token", e);
Timber.e(e, "Failed to get a new token");
return false;
}
}
this.token = token;
return true;
}
@Override
protected List<RepoModule> populate(JSONObject jsonObject) throws JSONException {
if (!jsonObject.getString("status").equals("success"))
throw new JSONException("Response is not a success!");
String name = jsonObject.optString(
"name", "Androidacy Modules Repo");
String nameForModules = name.endsWith(" (Official)") ?
name.substring(0, name.length() - 11) : name;
JSONArray jsonArray = jsonObject.getJSONArray("data");
Timber.d("AndroidacyRepoData populate start");
String name = jsonObject.optString("name", "Androidacy Modules Repo");
String nameForModules = name.endsWith(" (Official)") ? name.substring(0, name.length() - 11) : name;
JSONArray jsonArray;
try {
jsonArray = jsonObject.getJSONArray("data");
} catch (JSONException e) {
// probably using modules key since it's cached
try {
jsonArray = jsonObject.getJSONArray("modules");
} catch (JSONException e2) {
// we should never get here, bail out
Timber.e(e2, "Failed to parse modules");
return null;
}
}
for (RepoModule repoModule : this.moduleHashMap.values()) {
repoModule.processed = false;
}
@ -151,11 +321,20 @@ public final class AndroidacyRepoData extends RepoData {
long lastLastUpdate = 0;
for (int i = 0; i < len; i++) {
jsonObject = jsonArray.getJSONObject(i);
String moduleId = jsonObject.getString("codename");
// Deny remote modules ids shorter than 3 chars or containing null char or space
if (moduleId.length() < 3 || moduleId.indexOf('\0') != -1 ||
moduleId.indexOf(' ') != -1 || "ak3-helper".equals(moduleId)) continue;
long lastUpdate = jsonObject.getLong("updated_at") * 1000;
String moduleId;
try {
moduleId = jsonObject.getString("codename");
} catch (JSONException e) {
Timber.e("Module %s has no codename or json %s is invalid", jsonObject.optString("codename", "Unknown"), jsonObject.toString());
continue;
}
// Normally, we'd validate the module id here, but we don't need to because the server does it for us
long lastUpdate;
try {
lastUpdate = jsonObject.getLong("updated_at") * 1000;
} catch (JSONException e) {
lastUpdate = jsonObject.getLong("lastUpdate") * 1000;
}
lastLastUpdate = Math.max(lastLastUpdate, lastUpdate);
RepoModule repoModule = this.moduleHashMap.get(moduleId);
if (repoModule == null) {
@ -171,10 +350,8 @@ public final class AndroidacyRepoData extends RepoData {
repoModule.processed = true;
repoModule.lastUpdated = lastUpdate;
repoModule.repoName = nameForModules;
repoModule.zipUrl = filterURL(
jsonObject.optString("zipUrl", ""));
repoModule.notesUrl = filterURL(
jsonObject.optString("notesUrl", ""));
repoModule.zipUrl = filterURL(jsonObject.optString("zipUrl", ""));
repoModule.notesUrl = filterURL(jsonObject.optString("notesUrl", ""));
if (repoModule.zipUrl == null) {
repoModule.zipUrl = // Fallback url in case the API doesn't have zipUrl
"https://" + this.host + "/magisk/info/" + moduleId;
@ -187,13 +364,15 @@ public final class AndroidacyRepoData extends RepoData {
repoModule.notesUrl = this.injectToken(repoModule.notesUrl);
repoModule.qualityText = R.string.module_downloads;
repoModule.qualityValue = jsonObject.optInt("downloads", 0);
if (repoModule.qualityValue == 0) {
repoModule.qualityValue = jsonObject.optInt("stats", 0);
}
String checksum = jsonObject.optString("checksum", "");
repoModule.checksum = checksum.isEmpty() ? null : checksum;
ModuleInfo moduleInfo = repoModule.moduleInfo;
moduleInfo.name = jsonObject.getString("name");
moduleInfo.versionCode = jsonObject.getLong("versionCode");
moduleInfo.version = jsonObject.optString(
"version", "v" + moduleInfo.versionCode);
moduleInfo.version = jsonObject.optString("version", "v" + moduleInfo.versionCode);
moduleInfo.author = jsonObject.optString("author", "Unknown");
moduleInfo.description = jsonObject.optString("description", "");
moduleInfo.minApi = jsonObject.getInt("minApi");
@ -205,8 +384,7 @@ public final class AndroidacyRepoData extends RepoData {
moduleInfo.minMagisk = Integer.parseInt(minMagisk);
} else {
moduleInfo.minMagisk = // Allow 24.1 to mean 24100
(Integer.parseInt(minMagisk.substring(0, c)) * 1000) +
(Integer.parseInt(minMagisk.substring(c + 1)) * 100);
(Integer.parseInt(minMagisk.substring(0, c)) * 1000) + (Integer.parseInt(minMagisk.substring(c + 1)) * 100);
}
} catch (Exception e) {
moduleInfo.minMagisk = 0;
@ -216,11 +394,10 @@ public final class AndroidacyRepoData extends RepoData {
moduleInfo.mmtReborn = jsonObject.optBoolean("mmtReborn", false);
moduleInfo.support = filterURL(jsonObject.optString("support"));
moduleInfo.donate = filterURL(jsonObject.optString("donate"));
moduleInfo.safe = (jsonObject.has("vt_status") && jsonObject.getString("vt_status").equalsIgnoreCase("clean")) || jsonObject.optBoolean("safe", false);
String config = jsonObject.optString("config", "");
moduleInfo.config = config.isEmpty() ? null : config;
PropUtils.applyFallbacks(moduleInfo); // Apply fallbacks
Log.d(TAG, "Module " + moduleInfo.name + " " + moduleInfo.id + " " +
moduleInfo.version + " " + moduleInfo.versionCode);
}
Iterator<RepoModule> moduleInfoIterator = this.moduleHashMap.values().iterator();
while (moduleInfoIterator.hasNext()) {
@ -240,13 +417,6 @@ public final class AndroidacyRepoData extends RepoData {
return newModules;
}
private static String filterURL(String url) {
if (url == null || url.isEmpty() || PropUtils.isInvalidURL(url)) {
return null;
}
return url;
}
@Override
public void storeMetadata(RepoModule repoModule, byte[] data) {
}
@ -254,37 +424,34 @@ public final class AndroidacyRepoData extends RepoData {
@Override
public boolean tryLoadMetadata(RepoModule repoModule) {
if (this.moduleHashMap.containsKey(repoModule.id)) {
repoModule.moduleInfo.flags &=
~ModuleInfo.FLAG_METADATA_INVALID;
repoModule.moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID;
return true;
}
repoModule.moduleInfo.flags |=
ModuleInfo.FLAG_METADATA_INVALID;
repoModule.moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID;
return false;
}
@Override
public String getUrl() {
return this.token == null ? this.url :
this.url + "?token=" + this.token;
return token == null ? this.url : this.url + "?token=" + token + "&v=" + BuildConfig.VERSION_CODE + "&c=" + BuildConfig.VERSION_NAME + "&device_id=" + generateDeviceId() + "&client_id=" + BuildConfig.ANDROIDACY_CLIENT_ID;
}
private String injectToken(String url) {
// Do not inject token for non Androidacy urls
if (!AndroidacyUtil.isAndroidacyLink(url))
return url;
if (!AndroidacyUtil.isAndroidacyLink(url)) return url;
if (this.testMode) {
if (url.startsWith("https://api.androidacy.com/")) {
Log.e(TAG, "Got non test mode url: " + AndroidacyUtil.hideToken(url));
url = "https://staging-api.androidacy.com/" + url.substring(27);
if (url.startsWith("https://production-api.androidacy.com/")) {
Timber.e("Got non test mode url: %s", AndroidacyUtil.hideToken(url));
url = "https://staging-api.androidacy.com/" + url.substring(38);
}
} else {
if (url.startsWith("https://staging-api.androidacy.com/")) {
Log.e(TAG, "Got test mode url: " + AndroidacyUtil.hideToken(url));
url = "https://api.androidacy.com/" + url.substring(35);
Timber.e("Got test mode url: %s", AndroidacyUtil.hideToken(url));
url = "https://production-api.androidacy.com/" + url.substring(35);
}
}
String token = "token=" + this.token;
String token = "token=" + AndroidacyRepoData.token;
String deviceId = "device_id=" + generateDeviceId();
if (!url.contains(token)) {
if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
return url + '&' + token;
@ -292,6 +459,13 @@ public final class AndroidacyRepoData extends RepoData {
return url + '?' + token;
}
}
if (!url.contains(deviceId)) {
if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
return url + '&' + deviceId;
} else {
return url + '?' + deviceId;
}
}
return url;
}
@ -301,16 +475,9 @@ public final class AndroidacyRepoData extends RepoData {
return this.testMode ? super.getName() + " (Test Mode)" : super.getName();
}
String getToken() {
return this.token;
}
void setToken(String token) {
public void setToken(String token) {
if (Http.hasWebView()) {
CookieManager.getInstance().setCookie("https://.androidacy.com/",
"USER=" + token + "; expires=Fri, 31 Dec 9999 23:59:59 GMT;" +
" path=/; secure; domain=.androidacy.com");
this.token = token;
AndroidacyRepoData.token = token;
}
}
}

@ -5,7 +5,12 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class AndroidacyUtil {
import com.fox2code.mmm.BuildConfig;
import java.util.Objects;
public enum AndroidacyUtil {
;
public static final String REFERRER = "utm_source=FoxMMM&utm_medium=app";
public static boolean isAndroidacyLink(@Nullable Uri uri) {
@ -16,38 +21,86 @@ public class AndroidacyUtil {
return url != null && isAndroidacyLink(url, Uri.parse(url));
}
static boolean isAndroidacyLink(@NonNull String url,@NonNull Uri uri) {
static boolean isAndroidacyLink(@NonNull String url, @NonNull Uri uri) {
int i; // Check both string and Uri to mitigate parse exploit
return url.startsWith("https://") &&
(i = url.indexOf("/", 8)) != -1 &&
url.substring(8, i).endsWith(".androidacy.com") &&
uri.getHost().endsWith(".androidacy.com");
return url.startsWith("https://") && (i = url.indexOf("/", 8)) != -1 && url.substring(8, i).endsWith("api.androidacy.com") && Objects.requireNonNull(uri.getHost()).endsWith("api.androidacy.com");
}
public static boolean isAndroidacyFileUrl(@Nullable String url) {
if (url == null) return false;
for (String prefix : new String[]{
"https://production-api.androidacy.com/magisk/file/",
"https://staging-api.androidacy.com/magisk/file/"
}) { // Make both staging and non staging act the same
if (url.startsWith(prefix)) return true;
if (url == null)
return false;
for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://production-api.androidacy.com/magisk/file/", "https://staging-api.androidacy.com/magisk/file/"}) { // Make both staging and non staging act the same
if (url.startsWith(prefix))
return true;
}
return false;
}
// Avoid logging token
public static String hideToken(@NonNull String url) {
int i = url.lastIndexOf("token=");
if (i == -1) return url;
int i2 = url.indexOf('&', i);
int i3 = url.indexOf(' ', i);
if (i3 != -1 && i3 < i2) i2 = i3;
if (i2 == -1) {
return url.substring(0, i + 6) +
"<token>";
} else {
return url.substring(0, i + 6) +
"<token>" + url.substring(i2);
// for token, device_id, and client_id, replace with <hidden> by using replaceAll to match until the next non-alphanumeric character or end
// Also, URL decode
url = Uri.decode(url);
url = url + "&";
url = url.replaceAll("token=[^&]*", "token=<hidden>");
url = url.replaceAll("device_id=[^&]*", "device_id=<hidden>");
url = url.replaceAll("client_id=[^&]*", "client_id=<hidden>");
// remove last & added at the end
url = url.substring(0, url.length() - 1);
return url;
}
public static String getModuleId(String moduleUrl) {
// Get the &module= part
int i = moduleUrl.indexOf("&module=");
String moduleId;
// Match until next & or end
if (i != -1) {
int j = moduleUrl.indexOf('&', i + 1);
if (j == -1) {
moduleId = moduleUrl.substring(i + 8);
} else {
moduleId = moduleUrl.substring(i + 8, j);
}
// URL decode
moduleId = Uri.decode(moduleId);
// Strip non alphanumeric
moduleId = moduleId.replaceAll("[^a-zA-Z\\d]", "");
return moduleId;
}
if (BuildConfig.DEBUG) {
throw new IllegalArgumentException("Invalid module url: " + moduleUrl);
}
return null;
}
public static String getModuleTitle(String moduleUrl) {
// Get the &title= part
int i = moduleUrl.indexOf("&moduleTitle=");
// Match until next & or end
if (i != -1) {
int j = moduleUrl.indexOf('&', i + 1);
if (j == -1) {
return Uri.decode(moduleUrl.substring(i + 13));
} else {
return Uri.decode(moduleUrl.substring(i + 13, j));
}
}
return null;
}
public static String getChecksumFromURL(String moduleUrl) {
// Get the &version= part
int i = moduleUrl.indexOf("&checksum=");
// Match until next & or end
if (i != -1) {
int j = moduleUrl.indexOf('&', i + 1);
if (j == -1) {
return moduleUrl.substring(i + 10);
} else {
return moduleUrl.substring(i + 10, j);
}
}
return null;
}
}

@ -5,9 +5,7 @@ import android.content.res.Resources;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.widget.Button;
import android.widget.Toast;
@ -16,7 +14,7 @@ import androidx.annotation.Keep;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import com.fox2code.foxcompat.FoxDisplay;
import com.fox2code.foxcompat.view.FoxDisplay;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
@ -26,25 +24,26 @@ import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Hashes;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.Hashes;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import timber.log.Timber;
@SuppressWarnings({"unused", "SameReturnValue"})
@Keep
public class AndroidacyWebAPI {
public static final int COMPAT_UNSUPPORTED = 0;
public static final int COMPAT_DOWNLOAD = 1;
private static final String TAG = "AndroidacyWebAPI";
private static final int MAX_COMPAT_MODE = 1;
private final AndroidacyActivity activity;
private final boolean allowInstall;
private boolean allowHideNote = true;
boolean consumedAction;
boolean downloadMode;
int effectiveCompatMode;
@ -62,12 +61,17 @@ public class AndroidacyWebAPI {
this.downloadMode = false;
}
void openNativeModuleDialogRaw(String moduleUrl, String installTitle,
String checksum, boolean canInstall) {
Log.d(TAG, "ModuleDialog, downloadUrl: " + AndroidacyUtil.hideToken(moduleUrl));
void openNativeModuleDialogRaw(String moduleUrl, String moduleId, String installTitle, String checksum, boolean canInstall) {
if (BuildConfig.DEBUG)
Timber.d("ModuleDialog, downloadUrl: " + AndroidacyUtil.hideToken(moduleUrl) + ", moduleId: " + moduleId + ", installTitle: " + installTitle + ", checksum: " + checksum + ", canInstall: " + canInstall);
// moduleUrl should be a valid URL, i.e. in the androidacy.com domain
// if it is not, do not proceed
if (!AndroidacyUtil.isAndroidacyFileUrl(moduleUrl)) {
Timber.e("ModuleDialog, invalid URL: %s", moduleUrl);
return;
}
this.downloadMode = false;
RepoModule repoModule = AndroidacyRepoData
.getInstance().moduleHashMap.get(installTitle);
RepoModule repoModule = AndroidacyRepoData.getInstance().moduleHashMap.get(installTitle);
String title, description;
boolean mmtReborn = false;
if (repoModule != null) {
@ -78,19 +82,17 @@ public class AndroidacyWebAPI {
description = this.activity.getString(R.string.no_desc_found);
}
} else {
title = PropUtils.makeNameFromId(installTitle);
// URL Decode installTitle
title = installTitle;
String checkSumType = Hashes.checkSumName(checksum);
if (checkSumType == null) {
description = "Checksum: " + ((
checksum == null || checksum.isEmpty()) ? "null" : checksum);
description = "Checksum: " + ((checksum == null || checksum.isEmpty()) ? "null" : checksum);
} else {
description = checkSumType + ": " + checksum;
}
}
final MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(this.activity);
builder.setTitle(title).setMessage(description).setCancelable(true)
.setIcon(R.drawable.ic_baseline_extension_24);
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this.activity);
builder.setTitle(title).setMessage(description).setCancelable(true).setIcon(R.drawable.ic_baseline_extension_24);
builder.setNegativeButton(R.string.download_module, (x, y) -> {
this.downloadMode = true;
IntentHelper.openCustomTab(this.activity, moduleUrl);
@ -100,17 +102,16 @@ public class AndroidacyWebAPI {
String config = null;
if (repoModule != null) {
config = repoModule.moduleInfo.config;
LocalModuleInfo localModuleInfo =
ModuleManager.getINSTANCE().getModules().get(repoModule.id);
hasUpdate = localModuleInfo != null &&
repoModule.moduleInfo.versionCode > localModuleInfo.versionCode;
LocalModuleInfo localModuleInfo = ModuleManager.getINSTANCE().getModules().get(repoModule.id);
hasUpdate = localModuleInfo != null && repoModule.moduleInfo.versionCode > localModuleInfo.versionCode;
}
final String fModuleUrl = moduleUrl, fTitle = title,
fConfig = config, fChecksum = checksum;
final String fModuleUrl = moduleUrl, fTitle = title, fConfig = config, fChecksum = checksum;
final boolean fMMTReborn = mmtReborn;
builder.setPositiveButton(hasUpdate ?
R.string.update_module : R.string.install_module, (x, y) -> IntentHelper.openInstaller(this.activity,
fModuleUrl, fTitle, fConfig, fChecksum, fMMTReborn));
builder.setPositiveButton(hasUpdate ? R.string.update_module : R.string.install_module, (x, y) -> {
IntentHelper.openInstaller(this.activity, fModuleUrl, fTitle, fConfig, fChecksum, fMMTReborn);
// close activity
this.activity.runOnUiThread(this.activity::finishAndRemoveTask);
});
}
builder.setOnCancelListener(dialogInterface -> {
if (!this.activity.backOnResume)
@ -120,11 +121,10 @@ public class AndroidacyWebAPI {
this.downloadMode = true;
try {
return this.activity.downloadFileAsync(moduleUrl);
} catch (IOException e) {
Log.e(TAG, "Failed to download module", e);
AndroidacyWebAPI.this.activity.runOnUiThread(() ->
Toast.makeText(AndroidacyWebAPI.this.activity,
R.string.failed_download, Toast.LENGTH_SHORT).show());
} catch (
IOException e) {
Timber.e(e, "Failed to download module");
AndroidacyWebAPI.this.activity.runOnUiThread(() -> Toast.makeText(AndroidacyWebAPI.this.activity, R.string.failed_download, Toast.LENGTH_SHORT).show());
return null;
}
}, "androidacy_repo");
@ -142,8 +142,10 @@ public class AndroidacyWebAPI {
}
void notifyCompatModeRaw(int value) {
if (this.consumedAction) return;
Log.d(TAG, "Androidacy Compat mode: " + value);
if (this.consumedAction)
return;
if (BuildConfig.DEBUG)
Timber.d("Androidacy Compat mode: %s", value);
this.notifiedCompatMode = value;
if (value < 0) {
value = 0;
@ -156,7 +158,8 @@ public class AndroidacyWebAPI {
@JavascriptInterface
public void forceQuit(String error) {
// Allow forceQuit and cancel in downloadMode
if (this.consumedAction && !this.downloadMode) return;
if (this.consumedAction && !this.downloadMode)
return;
this.consumedAction = true;
this.forceQuitRaw(error);
}
@ -164,10 +167,10 @@ public class AndroidacyWebAPI {
@JavascriptInterface
public void cancel() {
// Allow forceQuit and cancel in downloadMode
if (this.consumedAction && !this.downloadMode) return;
if (this.consumedAction && !this.downloadMode)
return;
this.consumedAction = true;
this.activity.runOnUiThread(
this.activity::forceBackPressed);
this.activity.runOnUiThread(this.activity::forceBackPressed);
}
/**
@ -175,11 +178,13 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public void openUrl(String url) {
if (this.consumedAction) return;
if (this.consumedAction)
return;
this.consumedAction = true;
this.downloadMode = false;
Log.d(TAG, "Received openUrl request: " + url);
if (Uri.parse(url).getScheme().equals("https")) {
if (BuildConfig.DEBUG)
Timber.d("Received openUrl request: %s", url);
if (Objects.equals(Uri.parse(url).getScheme(), "https")) {
IntentHelper.openUrl(this.activity, url);
}
}
@ -189,11 +194,13 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public void openCustomTab(String url) {
if (this.consumedAction) return;
if (this.consumedAction)
return;
this.consumedAction = true;
this.downloadMode = false;
Log.d(TAG, "Received openCustomTab request: " + url);
if (Uri.parse(url).getScheme().equals("https")) {
if (BuildConfig.DEBUG)
Timber.d("Received openCustomTab request: %s", url);
if (Objects.equals(Uri.parse(url).getScheme(), "https")) {
IntentHelper.openCustomTab(this.activity, url);
}
}
@ -221,8 +228,7 @@ public class AndroidacyWebAPI {
@JavascriptInterface
public boolean canInstall() {
// With lockdown mode enabled or lack of root, install should not have any effect
return this.allowInstall && this.hasRoot() &&
!MainApplication.isShowcaseMode();
return this.allowInstall && this.hasRoot() && !MainApplication.isShowcaseMode();
}
/**
@ -236,31 +242,31 @@ public class AndroidacyWebAPI {
}
this.consumedAction = true;
this.downloadMode = false;
Log.d(TAG, "Received install request: " +
moduleUrl + " " + installTitle + " " + checksum);
if (BuildConfig.DEBUG)
Timber.d("Received install request: " + moduleUrl + " " + installTitle + " " + checksum);
if (!AndroidacyUtil.isAndroidacyLink(moduleUrl)) {
this.forceQuitRaw("Non Androidacy module link used on Androidacy");
return;
}
checksum = Hashes.checkSumFormat(checksum);
if (checksum == null || checksum.isEmpty()) {
Log.w(TAG, "Androidacy WebView didn't provided a checksum!");
Timber.w("Androidacy didn't provided a checksum!");
} else if (!Hashes.checkSumValid(checksum)) {
this.forceQuitRaw("Androidacy didn't provided a valid checksum");
return;
}
// moduleId is the module parameter in the url
String moduleId = AndroidacyUtil.getModuleId(moduleUrl);
// Let's handle download mode ourself if not implemented
if (this.effectiveCompatMode < 1) {
if (!this.canInstall()) {
this.downloadMode = true;
this.activity.runOnUiThread(() ->
this.activity.webView.loadUrl(moduleUrl));
this.activity.runOnUiThread(() -> this.activity.webView.loadUrl(moduleUrl));
} else {
this.openNativeModuleDialogRaw(moduleUrl, installTitle, checksum, true);
this.openNativeModuleDialogRaw(moduleUrl, moduleId, installTitle, checksum, true);
}
} else {
RepoModule repoModule = AndroidacyRepoData
.getInstance().moduleHashMap.get(installTitle);
RepoModule repoModule = AndroidacyRepoData.getInstance().moduleHashMap.get(installTitle);
String config = null;
boolean mmtReborn = false;
if (repoModule != null && repoModule.moduleInfo.name.length() >= 3) {
@ -269,8 +275,7 @@ public class AndroidacyWebAPI {
mmtReborn = repoModule.moduleInfo.mmtReborn;
}
this.activity.backOnResume = true;
IntentHelper.openInstaller(this.activity,
moduleUrl, installTitle, config, checksum, mmtReborn);
IntentHelper.openInstaller(this.activity, moduleUrl, installTitle, config, checksum, mmtReborn);
}
}
@ -279,7 +284,8 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public void openNativeModuleDialog(String moduleUrl, String moduleId, String checksum) {
if (this.consumedAction) return;
if (this.consumedAction)
return;
this.consumedAction = true;
this.downloadMode = false;
if (!AndroidacyUtil.isAndroidacyLink(moduleUrl)) {
@ -288,12 +294,14 @@ public class AndroidacyWebAPI {
}
checksum = Hashes.checkSumFormat(checksum);
if (checksum == null || checksum.isEmpty()) {
Log.w(TAG, "Androidacy WebView didn't provided a checksum!");
Timber.w("Androidacy WebView didn't provided a checksum!");
} else if (!Hashes.checkSumValid(checksum)) {
this.forceQuitRaw("Androidacy didn't provided a valid checksum");
return;
}
this.openNativeModuleDialogRaw(moduleUrl, moduleId, checksum, this.canInstall());
// Get moduleTitle from url
String moduleTitle = AndroidacyUtil.getModuleTitle(moduleUrl);
this.openNativeModuleDialogRaw(moduleUrl, moduleId, moduleTitle, checksum, this.canInstall());
}
/**
@ -336,16 +344,12 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public void hideActionBar() {
if (this.consumedAction) return;
if (this.consumedAction)
return;
this.consumedAction = true;
this.activity.runOnUiThread(() -> {
this.activity.hideActionBar();
this.consumedAction = false;
if (this.allowHideNote) {
this.allowHideNote = false;
this.activity.webViewNote
.setVisibility(View.GONE);
}
});
}
@ -355,7 +359,8 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public void showActionBar(final String title) {
if (this.consumedAction) return;
if (this.consumedAction)
return;
this.consumedAction = true;
this.activity.runOnUiThread(() -> {
this.activity.showActionBar();
@ -367,13 +372,13 @@ public class AndroidacyWebAPI {
}
/**
* Return true if the module is an Andoridacy module.
* Return true if the module is an Androidacy module.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
@JavascriptInterface
public boolean isAndroidacyModule(String moduleId) {
LocalModuleInfo localModuleInfo = ModuleManager.getINSTANCE().getModules().get(moduleId);
return localModuleInfo != null && ("Androidacy".equals(localModuleInfo.author) ||
AndroidacyUtil.isAndroidacyLink(localModuleInfo.config));
return localModuleInfo != null && ("Androidacy".equals(localModuleInfo.author) || AndroidacyUtil.isAndroidacyLink(localModuleInfo.config));
}
/**
@ -382,15 +387,18 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public String getAndroidacyModuleFile(String moduleId, String moduleFile) {
if (moduleFile == null || this.consumedAction ||
!this.isAndroidacyModule(moduleId)) return "";
moduleId = moduleId.replaceAll("\\.", "").replaceAll("/", "");
if (moduleFile == null || this.consumedAction || !this.isAndroidacyModule(moduleId))
return "";
moduleFile = moduleFile.replaceAll("\\.", "").replaceAll("/", "");
File moduleFolder = new File("/data/adb/modules/" + moduleId);
File absModuleFile = new File(moduleFolder, moduleFile).getAbsoluteFile();
if (!absModuleFile.getPath().startsWith(moduleFolder.getPath())) return "";
if (!absModuleFile.getPath().startsWith(moduleFolder.getPath()))
return "";
try {
return new String(Files.readSU(absModuleFile
.getAbsoluteFile()), StandardCharsets.UTF_8);
} catch (IOException e) {
return new String(Files.readSU(absModuleFile.getAbsoluteFile()), StandardCharsets.UTF_8);
} catch (
IOException e) {
return "";
}
}
@ -401,15 +409,15 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public boolean setAndroidacyModuleMeta(String moduleId, String content) {
if (content == null || this.consumedAction ||
!this.isAndroidacyModule(moduleId)) return false;
File androidacyMetaFile = new File(
"/data/adb/modules/" + moduleId + "/.androidacy");
moduleId = moduleId.replaceAll("\\.", "").replaceAll("/", "");
if (content == null || this.consumedAction || !this.isAndroidacyModule(moduleId))
return false;
File androidacyMetaFile = new File("/data/adb/modules/" + moduleId + "/.androidacy");
try {
Files.writeSU(androidacyMetaFile,
content.getBytes(StandardCharsets.UTF_8));
Files.writeSU(androidacyMetaFile, content.getBytes(StandardCharsets.UTF_8));
return true;
} catch (IOException e) {
} catch (
IOException e) {
return false;
}
}
@ -435,13 +443,12 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public int getMagiskVersionCode() {
return InstallerInitializer.peekMagiskPath() == null ? 0 :
InstallerInitializer.peekMagiskVersion();
return InstallerInitializer.peekMagiskPath() == null ? 0 : InstallerInitializer.peekMagiskVersion();
}
/**
* Return current android sdk-int version code, see:
* https://source.android.com/setup/start/build-numbers
* <a href="https://source.android.com/setup/start/build-numbers">right here</a>
*/
@JavascriptInterface
public int getAndroidVersionCode() {
@ -472,9 +479,8 @@ public class AndroidacyWebAPI {
public int getAccentColor() {
Resources.Theme theme = this.activity.getTheme();
TypedValue typedValue = new TypedValue();
theme.resolveAttribute(R.attr.colorPrimary, typedValue, true);
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
theme.resolveAttribute(androidx.appcompat.R.attr.colorPrimary, typedValue, true);
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
return typedValue.data;
}
theme.resolveAttribute(android.R.attr.colorAccent, typedValue, true);
@ -496,9 +502,8 @@ public class AndroidacyWebAPI {
public int getBackgroundColor() {
Resources.Theme theme = this.activity.getTheme();
TypedValue typedValue = new TypedValue();
theme.resolveAttribute(R.attr.backgroundColor, typedValue, true);
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
theme.resolveAttribute(com.google.android.material.R.attr.backgroundColor, typedValue, true);
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
return typedValue.data;
}
theme.resolveAttribute(android.R.attr.background, typedValue, true);
@ -510,11 +515,9 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public String getMonetColor(String id) {
@SuppressLint("DiscouragedApi") int nameResourceID = this.activity.getResources().getIdentifier("@android:color/" + id,
"color", this.activity.getApplicationInfo().packageName);
@SuppressLint("DiscouragedApi") int nameResourceID = this.activity.getResources().getIdentifier("@android:color/" + id, "color", this.activity.getApplicationInfo().packageName);
if (nameResourceID == 0) {
throw new IllegalArgumentException(
"No resource string found with name " + id);
throw new IllegalArgumentException("No resource string found with name " + id);
} else {
int color = ContextCompat.getColor(this.activity, nameResourceID);
int red = Color.red(color);

@ -5,6 +5,7 @@ import android.content.Context;
import android.content.Intent;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.utils.io.net.Http;
public class BackgroundBootListener extends BroadcastReceiver {
private static final String BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
@ -12,11 +13,15 @@ public class BackgroundBootListener extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!BOOT_COMPLETED.equals(intent.getAction())) return;
if (!MainApplication.isBackgroundUpdateCheckEnabled()) return;
if (!Http.hasConnectivity()) return;
// clear boot shared prefs
MainApplication.getBootSharedPreferences().edit().clear().apply();
synchronized (BackgroundUpdateChecker.lock) {
BackgroundUpdateChecker.onMainActivityCreate(context);
if (MainApplication.isBackgroundUpdateCheckEnabled()) {
new Thread(() -> {
BackgroundUpdateChecker.onMainActivityCreate(context);
BackgroundUpdateChecker.doCheck(context);
}
}).start();
}
}
}

@ -1,123 +1,249 @@
package com.fox2code.mmm.background;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.MainActivity;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.UpdateActivity;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.io.PropUtils;
import java.util.HashMap;
import java.util.Random;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import timber.log.Timber;
@SuppressWarnings("SpellCheckingInspection")
public class BackgroundUpdateChecker extends Worker {
private static boolean easterEggActive = false;
static final Object lock = new Object(); // Avoid concurrency issues
public static final String NOTIFICATION_CHANNEL_ID = "background_update";
public static final int NOTIFICATION_ID = 1;
public static final String NOTFIICATION_GROUP = "updates";
public static final String NOTIFICATION_CHANNEL_ID_APP = "background_update_app";
static final Object lock = new Object(); // Avoid concurrency issuespublic static final String NOTIFICATION_CHANNEL_ID = "background_update";
private static final int NOTIFICATION_ID_ONGOING = 2;
private static final String NOTIFICATION_CHANNEL_ID_ONGOING = "mmm_background_update";
private static final int NOTIFICATION_ID_APP = 3;
public BackgroundUpdateChecker(@NonNull Context context,
@NonNull WorkerParameters workerParams) {
public BackgroundUpdateChecker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
if (!NotificationManagerCompat.from(this.getApplicationContext()).areNotificationsEnabled()
|| !MainApplication.isBackgroundUpdateCheckEnabled()) return Result.success();
synchronized (lock) {
doCheck(this.getApplicationContext());
@SuppressLint("RestrictedApi")
private static void postNotificationForAppUpdate(Context context) {
// create the notification channel if not already created
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.createNotificationChannel(new NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL_ID_APP, NotificationManagerCompat.IMPORTANCE_HIGH).setName(context.getString(R.string.notification_channel_category_app_update)).setDescription(context.getString(R.string.notification_channel_category_app_update_description)).setGroup(NOTFIICATION_GROUP).build());
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_APP);
builder.setSmallIcon(R.drawable.baseline_system_update_24);
builder.setPriority(NotificationCompat.PRIORITY_HIGH);
builder.setCategory(NotificationCompat.CATEGORY_RECOMMENDATION);
builder.setShowWhen(false);
builder.setOnlyAlertOnce(true);
builder.setOngoing(false);
builder.setAutoCancel(true);
builder.setGroup(NOTFIICATION_GROUP);
// open app on click
Intent intent = new Intent(context, UpdateActivity.class);
// set action to ACTIONS.DOWNLOAD
intent.setAction(String.valueOf(UpdateActivity.ACTIONS.DOWNLOAD));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
builder.setContentIntent(android.app.PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE));
// set summary to Found X
builder.setContentTitle(context.getString(R.string.notification_channel_background_update_app));
builder.setContentText(context.getString(R.string.notification_channel_background_update_app_description));
if (ContextCompat.checkSelfPermission(MainApplication.getINSTANCE(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
notificationManager.notify(NOTIFICATION_ID_APP, builder.build());
}
return Result.success();
}
static void doCheck(Context context) {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
ModuleManager.getINSTANCE().scanAsync();
RepoManager.getINSTANCE().update(null);
ModuleManager.getINSTANCE().runAfterScan(() -> {
int moduleUpdateCount = 0;
HashMap<String, RepoModule> repoModules =
RepoManager.getINSTANCE().getModules();
for (LocalModuleInfo localModuleInfo :
ModuleManager.getINSTANCE().getModules().values()) {
if ("twrp-keep".equals(localModuleInfo.id)) continue;
RepoModule repoModule = repoModules.get(localModuleInfo.id);
localModuleInfo.checkModuleUpdate();
if (localModuleInfo.updateVersionCode > localModuleInfo.versionCode &&
!PropUtils.isNullString(localModuleInfo.updateVersion)) {
moduleUpdateCount++;
} else if (repoModule != null &&
repoModule.moduleInfo.versionCode > localModuleInfo.versionCode &&
!PropUtils.isNullString(repoModule.moduleInfo.version)) {
moduleUpdateCount++;
// first, check if the user has enabled background update checking
if (!MainApplication.getSharedPreferences("mmm").getBoolean("pref_background_update_check", false)) {
return;
}
if (MainApplication.getINSTANCE().isInForeground()) {
// don't check if app is in foreground, this is a background check
return;
}
// next, check if user requires wifi
if (MainApplication.getSharedPreferences("mmm").getBoolean("pref_background_update_check_wifi", true)) {
// check if wifi is connected
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network networkInfo = connectivityManager.getActiveNetwork();
if (networkInfo == null || !Objects.requireNonNull(connectivityManager.getNetworkCapabilities(networkInfo)).hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
Timber.w("Background update check: wifi not connected but required");
return;
}
}
synchronized (lock) {
// post checking notification if notifications are enabled
if (ContextCompat.checkSelfPermission(MainApplication.getINSTANCE(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.createNotificationChannel(new NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL_ID_ONGOING, NotificationManagerCompat.IMPORTANCE_MIN).setName(context.getString(R.string.notification_channel_category_background_update)).setDescription(context.getString(R.string.notification_channel_category_background_update_description)).setGroup(NOTFIICATION_GROUP).build());
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_ONGOING);
builder.setSmallIcon(R.drawable.ic_baseline_update_24);
builder.setPriority(NotificationCompat.PRIORITY_MIN);
builder.setCategory(NotificationCompat.CATEGORY_SERVICE);
builder.setShowWhen(false);
builder.setOnlyAlertOnce(true);
builder.setOngoing(true);
builder.setAutoCancel(false);
builder.setGroup("update_bg");
builder.setContentTitle(context.getString(R.string.notification_channel_background_update));
builder.setContentText(context.getString(R.string.notification_channel_background_update_description));
notificationManager.notify(NOTIFICATION_ID_ONGOING, builder.build());
} else {
Timber.d("Not posting notification because of missing permission");
}
ModuleManager.getINSTANCE().scanAsync();
RepoManager.getINSTANCE().update(null);
ModuleManager.getINSTANCE().runAfterScan(() -> {
int moduleUpdateCount = 0;
HashMap<String, RepoModule> repoModules = RepoManager.getINSTANCE().getModules();
// hashmap of updateable modules names
HashMap<String, String> updateableModules = new HashMap<>();
for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) {
if ("twrp-keep".equals(localModuleInfo.id)) continue;
// exclude all modules with id's stored in the pref pref_background_update_check_excludes
try {
if (Objects.requireNonNull(MainApplication.getSharedPreferences("mmm").getStringSet("pref_background_update_check_excludes", null)).contains(localModuleInfo.id))
continue;
} catch (Exception ignored) {
}
RepoModule repoModule = repoModules.get(localModuleInfo.id);
localModuleInfo.checkModuleUpdate();
if (localModuleInfo.updateVersionCode > localModuleInfo.versionCode && !PropUtils.isNullString(localModuleInfo.updateVersion)) {
moduleUpdateCount++;
updateableModules.put(localModuleInfo.name, localModuleInfo.version);
} else if (repoModule != null && repoModule.moduleInfo.versionCode > localModuleInfo.versionCode && !PropUtils.isNullString(repoModule.moduleInfo.version)) {
moduleUpdateCount++;
updateableModules.put(localModuleInfo.name, localModuleInfo.version);
}
}
if (moduleUpdateCount != 0) {
Timber.d("Found %d updates", moduleUpdateCount);
postNotification(context, updateableModules, moduleUpdateCount, false);
}
});
// check for app updates
if (MainApplication.getSharedPreferences("mmm").getBoolean("pref_background_update_check_app", false)) {
try {
boolean shouldUpdate = AppUpdateManager.getAppUpdateManager().checkUpdate(true);
if (shouldUpdate) {
Timber.d("Found app update");
postNotificationForAppUpdate(context);
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (moduleUpdateCount != 0) {
postNotification(context, moduleUpdateCount);
// remove checking notification
if (ContextCompat.checkSelfPermission(MainApplication.getINSTANCE(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
Timber.d("Removing notification");
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.cancel(NOTIFICATION_ID_ONGOING);
}
});
}
// increment or create counter in shared preferences
MainApplication.getSharedPreferences("mmm").edit().putInt("pref_background_update_counter", MainApplication.getSharedPreferences("mmm").getInt("pref_background_update_counter", 0) + 1).apply();
}
public static void postNotification(Context context, int updateCount) {
if (!easterEggActive) easterEggActive = new Random().nextInt(100) <= updateCount;
NotificationCompat.Builder builder = new NotificationCompat.Builder(
context, NOTIFICATION_CHANNEL_ID)
.setContentTitle(context.getString(easterEggActive ?
R.string.notification_update_title_easter_egg :
R.string.notification_update_title)
.replace("%i", String.valueOf(updateCount)))
.setContentText(context.getString(R.string.notification_update_subtitle))
.setSmallIcon(R.drawable.ic_baseline_extension_24)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(PendingIntent.getActivity(context, 0,
new Intent(context, MainActivity.class).setFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK),
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
PendingIntent.FLAG_IMMUTABLE : 0)).setAutoCancel(true);
public static void postNotification(Context context, HashMap<String, String> updateable, int updateCount, boolean test) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
builder.setSmallIcon(R.drawable.baseline_system_update_24);
builder.setPriority(NotificationCompat.PRIORITY_HIGH);
builder.setCategory(NotificationCompat.CATEGORY_RECOMMENDATION);
builder.setShowWhen(false);
builder.setOnlyAlertOnce(true);
builder.setOngoing(false);
builder.setAutoCancel(true);
builder.setGroup(NOTFIICATION_GROUP);
// open app on click
Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
builder.setContentIntent(android.app.PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE));
// set summary to Found X updates: <module name> <module version> <module name> <module version> ...
StringBuilder summary = new StringBuilder();
summary.append(context.getString(R.string.notification_update_summary));
// use notification_update_module_template string to set name and version
for (Map.Entry<String, String> entry : updateable.entrySet()) {
summary.append("\n").append(context.getString(R.string.notification_update_module_template, entry.getKey(), entry.getValue()));
}
builder.setContentTitle(context.getString(R.string.notification_update_title, updateCount));
builder.setContentText(summary);
// set long text to summary so it doesn't get cut off
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(summary));
if (ContextCompat.checkSelfPermission(MainApplication.getINSTANCE(), Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
return;
}
// check if app is in foreground. if so, don't show notification
if (MainApplication.getINSTANCE().isInForeground() && !test) return;
NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, builder.build());
}
public static void onMainActivityCreate(Context context) {
NotificationManagerCompat notificationManagerCompat =
NotificationManagerCompat.from(context);
notificationManagerCompat.createNotificationChannel(
new NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_HIGH).setShowBadge(true)
.setName(context.getString(R.string.notification_update_pref)).build());
// Refuse to run if first_launch pref is not false
if (!Objects.equals(MainApplication.getSharedPreferences("mmm").getString("last_shown_setup", null), "v2"))
return;
// create notification channel group
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence groupName = context.getString(R.string.notification_group_updates);
NotificationManager mNotificationManager = ContextCompat.getSystemService(context, NotificationManager.class);
Objects.requireNonNull(mNotificationManager).createNotificationChannelGroup(new NotificationChannelGroup(NOTFIICATION_GROUP, groupName));
}
NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(context);
notificationManagerCompat.createNotificationChannel(new NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH).setShowBadge(true).setName(context.getString(R.string.notification_update_pref)).setDescription(context.getString(R.string.auto_updates_notifs)).setGroup(NOTFIICATION_GROUP).build());
notificationManagerCompat.cancel(BackgroundUpdateChecker.NOTIFICATION_ID);
BackgroundUpdateChecker.easterEggActive = false;
WorkManager.getInstance(context).enqueueUniquePeriodicWork("background_checker",
ExistingPeriodicWorkPolicy.REPLACE, new PeriodicWorkRequest.Builder(
BackgroundUpdateChecker.class, 6, TimeUnit.HOURS)
.setConstraints(new Constraints.Builder().setRequiresBatteryNotLow(true)
.setRequiredNetworkType(NetworkType.UNMETERED).build()).build());
// now for the ongoing notification
notificationManagerCompat.createNotificationChannel(new NotificationChannelCompat.Builder(NOTIFICATION_CHANNEL_ID_ONGOING, NotificationManagerCompat.IMPORTANCE_MIN).setShowBadge(true).setName(context.getString(R.string.notification_update_pref)).setDescription(context.getString(R.string.auto_updates_notifs)).setGroup(NOTFIICATION_GROUP).build());
notificationManagerCompat.cancel(BackgroundUpdateChecker.NOTIFICATION_ID_ONGOING);
// schedule periodic check for updates every 6 hours (6 * 60 * 60 = 21600)
Timber.d("Scheduling periodic background check");
WorkManager.getInstance(context).enqueueUniquePeriodicWork("background_checker", ExistingPeriodicWorkPolicy.UPDATE, new PeriodicWorkRequest.Builder(BackgroundUpdateChecker.class, 6, TimeUnit.HOURS).setConstraints(new Constraints.Builder().setRequiresBatteryNotLow(true).build()).build());
}
public static void onMainActivityResume(Context context) {
NotificationManagerCompat.from(context).cancel(
BackgroundUpdateChecker.NOTIFICATION_ID);
BackgroundUpdateChecker.easterEggActive = false;
NotificationManagerCompat.from(context).cancel(BackgroundUpdateChecker.NOTIFICATION_ID);
}
@NonNull
@Override
public Result doWork() {
if (!NotificationManagerCompat.from(this.getApplicationContext()).areNotificationsEnabled() || !MainApplication.isBackgroundUpdateCheckEnabled())
return Result.success();
synchronized (lock) {
doCheck(this.getApplicationContext());
}
return Result.success();
}
}

@ -1,76 +1,89 @@
package com.fox2code.mmm.installer;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.os.PowerManager;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.Keep;
import androidx.recyclerview.widget.RecyclerView;
import com.fox2code.androidansi.AnsiConstants;
import com.fox2code.androidansi.AnsiParser;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainActivity;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.XHooks;
import com.fox2code.mmm.androidacy.AndroidacyUtil;
import com.fox2code.mmm.module.ActionButtonType;
import com.fox2code.mmm.sentry.SentryBreadcrumb;
import com.fox2code.mmm.sentry.SentryMain;
import com.fox2code.mmm.utils.FastException;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Hashes;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.Hashes;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.sentry.SentryBreadcrumb;
import com.fox2code.mmm.utils.sentry.SentryMain;
import com.google.android.material.bottomnavigation.BottomNavigationItemView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.topjohnwu.superuser.CallbackList;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import com.topjohnwu.superuser.io.SuFile;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.matomo.sdk.extra.TrackHelper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import timber.log.Timber;
public class InstallerActivity extends FoxActivity {
private static final String TAG = "InstallerActivity";
private static final HashSet<String> extracted = new HashSet<>();
public LinearProgressIndicator progressIndicator;
public ExtendedFloatingActionButton rebootFloatingButton;
public BottomNavigationItemView rebootFloatingButton;
public BottomNavigationItemView cancelFloatingButton;
public InstallerTerminal installerTerminal;
private File moduleCache;
private File toDelete;
private boolean textWrap;
private boolean canceled;
private boolean warnReboot;
private PowerManager.WakeLock wakeLock;
@SuppressLint("RestrictedApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
this.warnReboot = false;
this.moduleCache = new File(this.getCacheDir(), "installer");
if (!this.moduleCache.exists() && !this.moduleCache.mkdirs())
Log.e(TAG, "Failed to mkdir module cache dir!");
Timber.e("Failed to mkdir module cache dir!");
super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
this.setDisplayHomeAsUpEnabled(true);
setActionBarBackground(null);
this.setOnBackPressedCallback(a -> {
@ -78,7 +91,7 @@ public class InstallerActivity extends FoxActivity {
return false;
});
final Intent intent = this.getIntent();
final String target;
String target;
final String name;
final String checksum;
final boolean noExtensions;
@ -87,11 +100,16 @@ public class InstallerActivity extends FoxActivity {
// Should we allow 3rd part app to install modules?
if (Constants.INTENT_INSTALL_INTERNAL.equals(intent.getAction())) {
if (!MainApplication.checkSecret(intent)) {
Log.e(TAG, "Security check failed!");
Timber.e("Security check failed!");
this.forceBackPressed();
return;
}
// ensure the intent is from our app, and is either a url or within our directory. replace all instances of .. and url encoded ..
target = Objects.requireNonNull(intent.getStringExtra(Constants.EXTRA_INSTALL_PATH)).trim().replaceAll("\\.\\.", "").replaceAll("%2e%2e", "");
if (target.isEmpty() || !target.startsWith(MainApplication.getINSTANCE().getDataDir().getAbsolutePath()) && !target.startsWith("https://")) {
this.forceBackPressed();
return;
}
target = intent.getStringExtra(Constants.EXTRA_INSTALL_PATH);
name = intent.getStringExtra(Constants.EXTRA_INSTALL_NAME);
checksum = intent.getStringExtra(Constants.EXTRA_INSTALL_CHECKSUM);
noExtensions = intent.getBooleanExtra(// Allow intent to disable extensions
@ -105,7 +123,6 @@ public class InstallerActivity extends FoxActivity {
this.forceBackPressed();
return;
}
Log.i(TAG, "Install link: " + target);
// Note: Sentry only send this info on crash.
if (MainApplication.isCrashReportingEnabled()) {
SentryBreadcrumb breadcrumb = new SentryBreadcrumb();
@ -120,12 +137,10 @@ public class InstallerActivity extends FoxActivity {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setTitle(name);
this.textWrap = MainApplication.isTextWrapEnabled();
setContentView(this.textWrap ?
R.layout.installer_wrap : R.layout.installer);
setContentView(this.textWrap ? R.layout.installer_wrap : R.layout.installer);
int background;
int foreground;
if (MainApplication.getINSTANCE().isLightTheme() &&
!MainApplication.isForceDarkTerminal()) {
if (MainApplication.getINSTANCE().isLightTheme() && !MainApplication.isForceDarkTerminal()) {
background = Color.WHITE;
foreground = Color.BLACK;
} else {
@ -136,33 +151,37 @@ public class InstallerActivity extends FoxActivity {
RecyclerView installTerminal;
this.progressIndicator = findViewById(R.id.progress_bar);
this.rebootFloatingButton = findViewById(R.id.install_terminal_reboot_fab);
this.installerTerminal = new InstallerTerminal(
installTerminal = findViewById(R.id.install_terminal),
this.isLightTheme(), foreground, mmtReborn);
(horizontalScroller != null ? horizontalScroller : installTerminal)
.setBackground(new ColorDrawable(background));
this.cancelFloatingButton = findViewById(R.id.back_installer);
// disable both
this.rebootFloatingButton.setEnabled(false);
this.cancelFloatingButton.setEnabled(false);
this.installerTerminal = new InstallerTerminal(installTerminal = findViewById(R.id.install_terminal), this.isLightTheme(), foreground, mmtReborn);
(horizontalScroller != null ? horizontalScroller : installTerminal).setBackground(new ColorDrawable(background));
installTerminal.setItemAnimator(null);
this.progressIndicator.setVisibility(View.GONE);
this.progressIndicator.setIndeterminate(true);
this.getWindow().setFlags( // Note: Doesn't require WAKELOCK permission
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// acquire wakelock
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Fox:Installer");
this.progressIndicator.setVisibility(View.VISIBLE);
if (urlMode) this.installerTerminal.addLine("- Downloading " + name);
TrackHelper.track().event("installer_start", name).with(MainApplication.getINSTANCE().getTracker());
String finalTarget = target;
new Thread(() -> {
File moduleCache = this.toDelete = urlMode ?
new File(this.moduleCache, "module.zip") : new File(target);
if (urlMode && moduleCache.exists() && !moduleCache.delete() &&
!new SuFile(moduleCache.getAbsolutePath()).delete())
Log.e(TAG, "Failed to delete module cache");
// ensure module cache is is in our cache dir
if (urlMode && !moduleCache.getAbsolutePath().startsWith(MainApplication.getINSTANCE().getCacheDir().getAbsolutePath()))
throw new SecurityException("Module cache is not in cache dir!");
File moduleCache = this.toDelete = urlMode ? new File(this.moduleCache, "module.zip") : new File(finalTarget);
if (urlMode && moduleCache.exists() && !moduleCache.delete() && !new SuFile(moduleCache.getAbsolutePath()).delete())
Timber.e("Failed to delete module cache");
String errMessage = "Failed to download module zip";
// Set this to the error message if it's a HTTP error
byte[] rawModule;
boolean androidacyBlame = false; // In case Androidacy mess-up again...
try {
Log.i(TAG, (urlMode ? "Downloading: " : "Loading: ") + target);
rawModule = urlMode ? Http.doHttpGet(target, (progress, max, done) -> {
if (max <= 0 && this.progressIndicator.isIndeterminate())
return;
Timber.i("%s%s", (urlMode ? "Downloading: " : "Loading: "), AndroidacyUtil.hideToken(finalTarget));
rawModule = urlMode ? Http.doHttpGet(finalTarget, (progress, max, done) -> {
if (max <= 0 && this.progressIndicator.isIndeterminate()) return;
this.runOnUiThread(() -> {
this.progressIndicator.setIndeterminate(false);
this.progressIndicator.setMax(max);
@ -174,28 +193,40 @@ public class InstallerActivity extends FoxActivity {
this.progressIndicator.setIndeterminate(true);
});
if (this.canceled) return;
androidacyBlame = urlMode && AndroidacyUtil.isAndroidacyFileUrl(target);
if (checksum != null && !checksum.isEmpty()) {
Log.d(TAG, "Checking for checksum: " + checksum);
//noinspection UnnecessaryCallToStringValueOf
Timber.i("Checking for checksum: %s", String.valueOf(checksum));
this.runOnUiThread(() -> this.installerTerminal.addLine("- Checking file integrity"));
if (!Hashes.checkSumMatch(rawModule, checksum)) {
this.setInstallStateFinished(false,
"! File integrity check failed", "");
this.setInstallStateFinished(false, "! File integrity check failed", "");
return;
}
}
if (this.canceled) return;
Files.fixJavaZipHax(rawModule);
// checks to make sure zip is not a source archive, and if it is, unzips the folder within, switches to it, and zips up the contents of it
Files.fixSourceArchiveShit(rawModule);
boolean noPatch = false;
boolean isModule = false;
boolean isAnyKernel3 = false;
boolean isInstallZipModule = false;
errMessage = "File is not a valid zip file";
try (ZipInputStream zipInputStream = new ZipInputStream(
new ByteArrayInputStream(rawModule))) {
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
// use apache commons to unzip the zip file, with a try-with-resources to ensure it's closed
// write the zip file to a temporary file
File zipFileTemp = new File(this.getCacheDir(), "module.zip");
try (FileOutputStream fos = new FileOutputStream(zipFileTemp)) {
fos.write(rawModule);
}
try (ZipFile zipFile = new ZipFile(zipFileTemp)) {
// get the zip entries
Enumeration<? extends ZipEntry> zipEntries = zipFile.getEntries();
// iterate over the zip entries
while (zipEntries.hasMoreElements()) {
// get the next zip entry
ZipEntry zipEntry = zipEntries.nextElement();
// get the name of the zip entry
String entryName = zipEntry.getName();
// check if the zip entry is a directory
if (entryName.equals("tools/ak3-core.sh")) {
noPatch = true;
isAnyKernel3 = true;
@ -204,30 +235,28 @@ public class InstallerActivity extends FoxActivity {
noPatch = true;
isModule = true;
break;
} if (entryName.equals("META-INF/com/google/android/magisk/module.prop")) {
}
if (entryName.equals("META-INF/com/google/android/magisk/module.prop")) {
noPatch = true;
isInstallZipModule = true;
break;
} else if (entryName.endsWith("/tools/ak3-core.sh")) {
isAnyKernel3 = true;
} else if (entryName.endsWith(
"/META-INF/com/google/android/magisk/module.prop")) {
} else if (entryName.endsWith("/META-INF/com/google/android/update-binary")) {
isInstallZipModule = true;
} else if (entryName.endsWith("/module.prop")) {
isModule = true;
}
}
} catch (IOException e) {
Timber.e(e, "Failed to read zip file");
this.setInstallStateFinished(false, errMessage, "");
return;
}
if (!isModule && !isAnyKernel3 && !isInstallZipModule) {
if (androidacyBlame) {
this.installerTerminal.addLine(
"! Note: The following error is probably an Androidacy backend error");
}
this.setInstallStateFinished(false,
"! File is not a valid Magisk module or AnyKernel3 zip", "");
this.setInstallStateFinished(false, "! File is not a valid Magisk module or AnyKernel3 zip", "");
return;
}
androidacyBlame = false;
if (noPatch) {
if (urlMode) {
errMessage = "Failed to save module zip";
@ -239,7 +268,6 @@ public class InstallerActivity extends FoxActivity {
} else {
errMessage = "Failed to patch module zip";
this.runOnUiThread(() -> this.installerTerminal.addLine("- Patching " + name));
Log.i(TAG, "Patching: " + moduleCache.getName());
try (OutputStream outputStream = new FileOutputStream(moduleCache)) {
Files.patchModuleSimple(rawModule, outputStream);
outputStream.flush();
@ -252,69 +280,51 @@ public class InstallerActivity extends FoxActivity {
errMessage = "Failed to install module zip";
this.doInstall(moduleCache, noExtensions, rootless);
} catch (IOException e) {
Log.e(TAG, errMessage, e);
if (androidacyBlame) {
this.installerTerminal.addLine(
"! Note: The following error is probably an Androidacy backend error");
}
this.setInstallStateFinished(false,
"! " + errMessage, "");
Timber.e(e);
this.setInstallStateFinished(false, errMessage, null);
} catch (OutOfMemoryError e) {
//noinspection UnusedAssignment (Important to avoid OutOfMemoryError)
rawModule = null; // Because reference is kept when calling setInstallStateFinished
if ("Failed to install module zip".equals(errMessage))
throw e; // Ignore if in installation state.
Log.e(TAG, "Module too large", e);
this.setInstallStateFinished(false,
"! Module is too large to be loaded on this device", "");
Timber.e(e);
this.setInstallStateFinished(false, "! Module is too large to be loaded on this device", "");
}
}, "Module install Thread").start();
}
@SuppressWarnings("SpellCheckingInspection")
@Keep
private void doInstall(File file, boolean noExtensions, boolean rootless) {
if (this.canceled) return;
UiThreadHandler.runAndWait(() -> {
this.setOnBackPressedCallback(DISABLE_BACK_BUTTON);
this.setDisplayHomeAsUpEnabled(false);
});
Log.i(TAG, "Installing: " + moduleCache.getName());
InstallerController installerController = new InstallerController(
this.progressIndicator, this.installerTerminal,
file.getAbsoluteFile(), noExtensions);
Timber.i("Installing: %s", moduleCache.getName());
InstallerController installerController = new InstallerController(this.progressIndicator, this.installerTerminal, file.getAbsoluteFile(), noExtensions);
InstallerMonitor installerMonitor;
Shell.Job installJob;
if (rootless) { // rootless is only used for debugging
File installScript = this.extractInstallScript("module_installer_test.sh");
if (installScript == null) {
this.setInstallStateFinished(false,
"! Failed to extract test install script", "");
this.setInstallStateFinished(false, "! Failed to extract test install script", "");
return;
}
this.installerTerminal.enableAnsi();
// Extract customize.sh manually in rootless mode because unzip might not exists
try (ZipFile zipFile = new ZipFile(file)) {
ZipEntry zipEntry = zipFile.getEntry("customize.sh");
ZipArchiveEntry zipEntry = zipFile.getEntry("customize.sh");
if (zipEntry != null) {
try (FileOutputStream fileOutputStream = new FileOutputStream(
new File(file.getParentFile(), "customize.sh"))) {
try (FileOutputStream fileOutputStream = new FileOutputStream(new File(file.getParentFile(), "customize.sh"))) {
Files.copy(zipFile.getInputStream(zipEntry), fileOutputStream);
}
}
} catch (Exception e) {
Log.d(TAG, "Failed ot extract install script via java code", e);
Timber.i(e);
}
installerMonitor = new InstallerMonitor(installScript);
installJob = Shell.cmd("export MMM_EXT_SUPPORT=1",
"export MMM_USER_LANGUAGE=" + this.getResources()
.getConfiguration().locale.toLanguageTag(),
"export MMM_APP_VERSION=" + BuildConfig.VERSION_NAME,
"export MMM_TEXT_WRAP=" + (this.textWrap ? "1" : "0"),
AnsiConstants.ANSI_CMD_SUPPORT,
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
"sh \"" + installScript.getAbsolutePath() + "\"" +
" 3 0 \"" + file.getAbsolutePath() + "\"")
.to(installerController, installerMonitor);
installJob = Shell.cmd("export MMM_EXT_SUPPORT=1", "export MMM_USER_LANGUAGE=" + this.getResources().getConfiguration().getLocales().get(0).toLanguageTag(), "export MMM_APP_VERSION=" + BuildConfig.VERSION_NAME, "export MMM_TEXT_WRAP=" + (this.textWrap ? "1" : "0"), AnsiConstants.ANSI_CMD_SUPPORT, "cd \"" + this.moduleCache.getAbsolutePath() + "\"", "sh \"" + installScript.getAbsolutePath() + "\"" + " 3 0 \"" + file.getAbsolutePath() + "\"").to(installerController, installerMonitor);
} else {
String arch32 = "true"; // Do nothing by default
boolean needs32bit = false;
@ -332,11 +342,9 @@ public class InstallerActivity extends FoxActivity {
try (ZipFile zipFile = new ZipFile(file)) {
// Check if module is AnyKernel module
if (zipFile.getEntry("tools/ak3-core.sh") != null) {
ZipEntry updateBinary = zipFile.getEntry(
"META-INF/com/google/android/update-binary");
ZipArchiveEntry updateBinary = zipFile.getEntry("META-INF/com/google/android/update-binary");
if (updateBinary != null) {
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(zipFile.getInputStream(updateBinary)));
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(zipFile.getInputStream(updateBinary)));
String line;
while ((line = bufferedReader.readLine()) != null) {
if (line.contains("AnyKernel3")) {
@ -348,60 +356,42 @@ public class InstallerActivity extends FoxActivity {
}
}
if ((zipFile.getEntry( // Check if module hard require 32bit support
"common/addon/Volume-Key-Selector/tools/arm64/keycheck") == null &&
zipFile.getEntry("common/addon/Volume-Key-Selector/install.sh") != null) ||
(zipFile.getEntry("META-INF/zbin/keycheck_arm64") == null &&
zipFile.getEntry("META-INF/zbin/keycheck_arm") != null)) {
"common/addon/Volume-Key-Selector/tools/arm64/keycheck") == null && zipFile.getEntry("common/addon/Volume-Key-Selector/install.sh") != null) || (zipFile.getEntry("META-INF/zbin/keycheck_arm64") == null && zipFile.getEntry("META-INF/zbin/keycheck_arm") != null)) {
needs32bit = true;
}
ZipEntry moduleProp = zipFile.getEntry("module.prop");
ZipArchiveEntry moduleProp = zipFile.getEntry("module.prop");
magiskModule = moduleProp != null;
if (zipFile.getEntry("install.sh") == null &&
zipFile.getEntry("customize.sh") == null &&
zipFile.getEntry("setup.sh") != null && magiskModule) {
if (zipFile.getEntry("install.sh") == null && zipFile.getEntry("customize.sh") == null && zipFile.getEntry("setup.sh") != null && magiskModule) {
mmtReborn = true; // MMT-Reborn require a separate runtime
}
if (!magiskModule && (moduleProp = zipFile.getEntry(
"META-INF/com/google/android/magisk/module.prop")) != null) {
if (!magiskModule && (moduleProp = zipFile.getEntry("META-INF/com/google/android/magisk/module.prop")) != null) {
installZipMagiskModule = true;
}
moduleId = PropUtils.readModuleId(zipFile.getInputStream(moduleProp));
} catch (IOException ignored) {
}
int compatFlags = AppUpdateManager.getFlagsForModule(moduleId);
if ((compatFlags & AppUpdateManager.FLAG_COMPAT_NEED_32BIT) != 0)
needs32bit = true;
if ((compatFlags & AppUpdateManager.FLAG_COMPAT_NO_EXT) != 0)
noExtensions = true;
if (moduleId != null && (moduleId.isEmpty() ||
moduleId.contains("/") || moduleId.contains("\0") ||
(moduleId.startsWith(".") && moduleId.endsWith(".")))) {
this.setInstallStateFinished(false,
"! This module contain a dangerous moduleId",
null);
if ((compatFlags & AppUpdateManager.FLAG_COMPAT_NEED_32BIT) != 0) needs32bit = true;
if ((compatFlags & AppUpdateManager.FLAG_COMPAT_NO_EXT) != 0) noExtensions = true;
if (moduleId != null && (moduleId.isEmpty() || moduleId.contains("/") || moduleId.contains("\0") || (moduleId.startsWith(".") && moduleId.endsWith(".")))) {
this.setInstallStateFinished(false, "! This module contain a dangerous moduleId", null);
return;
}
if (magiskModule && moduleId == null && !anyKernel3) {
// Modules without module Ids are module installed by 3rd party software
this.setInstallStateFinished(false,
"! Magisk modules require a moduleId", null);
this.setInstallStateFinished(false, "! Magisk modules require a moduleId", null);
return;
}
if (anyKernel3) {
installerController.useRecoveryExt();
} else if (Build.SUPPORTED_32_BIT_ABIS.length == 0) {
if (needs32bit) {
this.setInstallStateFinished(false,
"! This module can't be installed on a 64bit only system",
null);
return;
}
} else if (needs32bit || (compatFlags & AppUpdateManager.FLAG_COMPAT_NO_EXT) == 0) {
// Restore Magisk legacy stuff for retro compatibility
if (Build.SUPPORTED_32_BIT_ABIS[0].contains("arm"))
arch32 = "export ARCH32=arm";
if (Build.SUPPORTED_32_BIT_ABIS[0].contains("x86"))
arch32 = "export ARCH32=x86";
if (Build.SUPPORTED_32_BIT_ABIS.length > 0) {
if (Build.SUPPORTED_32_BIT_ABIS[0].contains("arm"))
arch32 = "export ARCH32=arm";
if (Build.SUPPORTED_32_BIT_ABIS[0].contains("x86"))
arch32 = "export ARCH32=x86";
}
}
String installCommand;
File installExecutable;
@ -410,47 +400,32 @@ public class InstallerActivity extends FoxActivity {
this.warnReboot = true; // We should probably re-flash magisk...
installExecutable = this.extractInstallScript("anykernel3_installer.sh");
if (installExecutable == null) {
this.setInstallStateFinished(false,
"! Failed to extract AnyKernel3 install script", "");
this.setInstallStateFinished(false, "! Failed to extract AnyKernel3 install script", "");
return;
}
// "unshare -m" is needed to force mount namespace isolation.
// This allow AnyKernel to mess-up with mounts point without crashing the system!
installCommand = "unshare -m " + ASH + " \"" +
installExecutable.getAbsolutePath() + "\"" +
" 3 1 \"" + file.getAbsolutePath() + "\"";
} else if (installZipMagiskModule ||
(compatFlags & AppUpdateManager.FLAG_COMPAT_ZIP_WRAPPER) != 0) {
installCommand = "unshare -m " + ASH + " \"" + installExecutable.getAbsolutePath() + "\"" + " 3 1 \"" + file.getAbsolutePath() + "\"";
} else if (installZipMagiskModule || (compatFlags & AppUpdateManager.FLAG_COMPAT_ZIP_WRAPPER) != 0) {
installExecutable = this.extractInstallScript("module_installer_wrapper.sh");
if (installExecutable == null) {
this.setInstallStateFinished(false,
"! Failed to extract Magisk module wrapper script", "");
this.setInstallStateFinished(false, "! Failed to extract Magisk module wrapper script", "");
return;
}
installCommand = ASH + " \"" +
installExecutable.getAbsolutePath() + "\"" +
" 3 1 \"" + file.getAbsolutePath() + "\"";
} else if (InstallerInitializer.peekMagiskVersion() >=
Constants.MAGISK_VER_CODE_INSTALL_COMMAND &&
((compatFlags & AppUpdateManager.FLAG_COMPAT_MAGISK_CMD) != 0 ||
noExtensions || MainApplication.isUsingMagiskCommand())) {
installCommand = ASH + " \"" + installExecutable.getAbsolutePath() + "\"" + " 3 1 \"" + file.getAbsolutePath() + "\"";
} else if (InstallerInitializer.peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND && ((compatFlags & AppUpdateManager.FLAG_COMPAT_MAGISK_CMD) != 0 || noExtensions || MainApplication.isUsingMagiskCommand())) {
installCommand = "magisk --install-module \"" + file.getAbsolutePath() + "\"";
installExecutable = new File(MAGISK_PATH.equals("/sbin") ?
"/sbin/magisk" : "/system/bin/magisk");
installExecutable = new File(MAGISK_PATH.equals("/sbin") ? "/sbin/magisk" : "/system/bin/magisk");
magiskCmdLine = true;
} else if (moduleId != null) {
installExecutable = this.extractInstallScript("module_installer_compat.sh");
if (installExecutable == null) {
this.setInstallStateFinished(false,
"! Failed to extract Magisk module install script", "");
this.setInstallStateFinished(false, "! Failed to extract Magisk module install script", "");
return;
}
installCommand = ASH + " \"" +
installExecutable.getAbsolutePath() + "\"" +
" 3 1 \"" + file.getAbsolutePath() + "\"";
installCommand = ASH + " \"" + installExecutable.getAbsolutePath() + "\"" + " 3 1 \"" + file.getAbsolutePath() + "\"";
} else {
this.setInstallStateFinished(false,
"! Zip file is not a valid Magisk module or AnyKernel3 zip!", "");
this.setInstallStateFinished(false, "! Zip file is not a valid Magisk module or AnyKernel3 zip!", "");
return;
}
installerMonitor = new InstallerMonitor(installExecutable);
@ -460,26 +435,12 @@ public class InstallerActivity extends FoxActivity {
this.installerTerminal.enableAnsi();
else this.installerTerminal.disableAnsi();
installJob = Shell.cmd(arch32, "export BOOTMODE=true", // No Extensions
this.installerTerminal.isAnsiEnabled() ?
AnsiConstants.ANSI_CMD_SUPPORT : "true",
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
installCommand).to(installerController, installerMonitor);
this.installerTerminal.isAnsiEnabled() ? AnsiConstants.ANSI_CMD_SUPPORT : "true", "cd \"" + this.moduleCache.getAbsolutePath() + "\"", installCommand).to(installerController, installerMonitor);
} else {
if ((compatFlags & AppUpdateManager.FLAG_COMPAT_NO_ANSI) != 0)
this.installerTerminal.disableAnsi();
else this.installerTerminal.enableAnsi();
installJob = Shell.cmd(arch32, "export MMM_EXT_SUPPORT=1",
"export MMM_USER_LANGUAGE=" + this.getResources()
.getConfiguration().locale.toLanguageTag(),
"export MMM_APP_VERSION=" + BuildConfig.VERSION_NAME,
"export MMM_TEXT_WRAP=" + (this.textWrap ? "1" : "0"),
this.installerTerminal.isAnsiEnabled() ?
AnsiConstants.ANSI_CMD_SUPPORT : "true",
mmtReborn ? "export MMM_MMT_REBORN=1" : "true",
"export BOOTMODE=true", anyKernel3 ? "export AK3TMPFS=" +
InstallerInitializer.peekMagiskPath() + "/ak3tmpfs" :
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
installCommand).to(installerController, installerMonitor);
installJob = Shell.cmd(arch32, "export MMM_EXT_SUPPORT=1", "export MMM_USER_LANGUAGE=" + this.getResources().getConfiguration().getLocales().get(0).toLanguageTag(), "export MMM_APP_VERSION=" + BuildConfig.VERSION_NAME, "export MMM_TEXT_WRAP=" + (this.textWrap ? "1" : "0"), this.installerTerminal.isAnsiEnabled() ? AnsiConstants.ANSI_CMD_SUPPORT : "true", mmtReborn ? "export MMM_MMT_REBORN=1" : "true", "export BOOTMODE=true", anyKernel3 ? "export AK3TMPFS=" + InstallerInitializer.peekMagiskPath() + "/ak3tmpfs" : "cd \"" + this.moduleCache.getAbsolutePath() + "\"", installCommand).to(installerController, installerMonitor);
}
// Note: Sentry only send this info on crash.
if (MainApplication.isCrashReportingEnabled()) {
@ -490,18 +451,18 @@ public class InstallerActivity extends FoxActivity {
breadcrumb.setData("isAnyKernel3", anyKernel3 ? "true" : "false");
breadcrumb.setData("noExtensions", noExtensions ? "true" : "false");
breadcrumb.setData("magiskCmdLine", magiskCmdLine ? "true" : "false");
breadcrumb.setData("ansi", this.installerTerminal
.isAnsiEnabled() ? "enabled" : "disabled");
breadcrumb.setData("ansi", this.installerTerminal.isAnsiEnabled() ? "enabled" : "disabled");
breadcrumb.setCategory("app.action.install");
SentryMain.addSentryBreadcrumb(breadcrumb);
}
if (mmtReborn && magiskCmdLine) {
Log.w(TAG, "mmtReborn and magiskCmdLine may not work well together");
Timber.w("mmtReborn and magiskCmdLine may not work well together");
}
}
boolean success = installJob.exec().isSuccess();
// Wait one UI cycle before disabling controller or processing results
UiThreadHandler.runAndWait(() -> {}); // to avoid race conditions
UiThreadHandler.runAndWait(() -> {
}); // to avoid race conditions
installerController.disable();
String message = "- Install successful";
if (!success) {
@ -512,8 +473,96 @@ public class InstallerActivity extends FoxActivity {
message = installerMonitor.doCleanUp();
}
}
this.setInstallStateFinished(success, message,
installerController.getSupportLink());
this.setInstallStateFinished(success, message, installerController.getSupportLink());
}
private File extractInstallScript(String script) {
File compatInstallScript = new File(this.moduleCache, script);
if (!compatInstallScript.exists() || compatInstallScript.length() == 0 || !extracted.contains(script)) {
try {
Files.write(compatInstallScript, Files.readAllBytes(this.getAssets().open(script)));
extracted.add(script);
} catch (IOException e) {
if (compatInstallScript.delete()) extracted.remove(script);
Timber.e(e);
return null;
}
}
return compatInstallScript;
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
return true;
return super.dispatchKeyEvent(event);
}
@SuppressLint("RestrictedApi")
@SuppressWarnings("SameParameterValue")
private void setInstallStateFinished(boolean success, String message, String optionalLink) {
this.installerTerminal.disableAnsi();
if (success && toDelete != null && !toDelete.delete()) {
SuFile suFile = new SuFile(toDelete.getAbsolutePath());
if (suFile.exists() && !suFile.delete()) Timber.w("Failed to delete zip file");
else toDelete = null;
} else toDelete = null;
this.runOnUiThread(() -> {
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, 0);
// release wakelock
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
wakeLock = null;
}
// Set the back press to finish the activity and return to the main activity
this.setOnBackPressedCallback(a -> {
this.finishAndRemoveTask();
startActivity(new Intent(this, MainActivity.class));
return true;
});
this.setDisplayHomeAsUpEnabled(true);
this.progressIndicator.setVisibility(View.GONE);
// This should be improved ?
String reboot_cmd = "/system/bin/svc power reboot || /system/bin/reboot || setprop sys.powerctl reboot";
this.rebootFloatingButton.setOnClickListener(_view -> {
if (this.warnReboot || MainApplication.shouldPreventReboot()) {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.install_terminal_reboot_now).setMessage(R.string.install_terminal_reboot_now_message).setCancelable(false).setIcon(R.drawable.ic_reboot_24).setPositiveButton(R.string.ok, (x, y) -> Shell.cmd(reboot_cmd).submit()).setNegativeButton(R.string.no, (x, y) -> x.dismiss()).show();
} else {
Shell.cmd(reboot_cmd).submit();
}
});
this.rebootFloatingButton.setEnabled(true);
this.cancelFloatingButton.setEnabled(true);
// handle back button
this.cancelFloatingButton.setOnClickListener(_view -> this.forceBackPressed());
if (message != null && !message.isEmpty()) this.installerTerminal.addLine(message);
if (optionalLink != null && !optionalLink.isEmpty()) {
this.setActionBarExtraMenuButton(ActionButtonType.supportIconForUrl(optionalLink), menu -> {
IntentHelper.openUrl(this, optionalLink);
return true;
});
} else if (success) {
final Intent intent = this.getIntent();
final String config = MainApplication.checkSecret(intent) ? intent.getStringExtra(Constants.EXTRA_INSTALL_CONFIG) : null;
if (config != null && !config.isEmpty()) {
String configPkg = IntentHelper.getPackageOfConfig(config);
try {
XHooks.checkConfigTargetExists(this, configPkg, config);
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, menu -> {
IntentHelper.openConfig(this, config);
return true;
});
} catch (PackageManager.NameNotFoundException e) {
Timber.w("Config package \"" + configPkg + "\" missing for installer view");
this.installerTerminal.addLine(String.format(this.getString(R.string.install_terminal_config_missing), configPkg));
}
}
}
});
}
public static class InstallerController extends CallbackList<String> {
@ -521,13 +570,10 @@ public class InstallerActivity extends FoxActivity {
private final InstallerTerminal terminal;
private final File moduleFile;
private final boolean noExtension;
private boolean enabled, useExt,
useRecovery, isRecoveryBar;
private boolean enabled, useExt, useRecovery, isRecoveryBar;
private String supportLink = "";
private InstallerController(LinearProgressIndicator progressIndicator,
InstallerTerminal terminal, File moduleFile,
boolean noExtension) {
private InstallerController(LinearProgressIndicator progressIndicator, InstallerTerminal terminal, File moduleFile, boolean noExtension) {
this.progressIndicator = progressIndicator;
this.terminal = terminal;
this.moduleFile = moduleFile;
@ -539,7 +585,7 @@ public class InstallerActivity extends FoxActivity {
@Override
public void onAddElement(String s) {
if (!this.enabled) return;
Log.d(TAG, "MSG: " + s);
Timber.i("MSG: %s", s);
if ("#!useExt".equals(s.trim()) && !this.noExtension) {
this.useExt = true;
return;
@ -564,11 +610,10 @@ public class InstallerActivity extends FoxActivity {
this.processCommand("showLoading 256");
this.processCommand("setLoading " + progressInt);
this.isRecoveryBar = true;
} catch (Exception ignored) {}
} catch (Exception ignored) {
}
} else {
this.terminal.addLine(s.replace(
this.moduleFile.getAbsolutePath(),
this.moduleFile.getName()));
this.terminal.addLine(s.replace(this.moduleFile.getAbsolutePath(), this.moduleFile.getName()));
}
}
@ -584,25 +629,13 @@ public class InstallerActivity extends FoxActivity {
command = rawCommand;
}
switch (command) {
case "useRecovery":
this.useRecovery = true;
break;
case "addLine":
this.terminal.addLine(arg);
break;
case "setLastLine":
this.terminal.setLastLine(arg);
break;
case "clearTerminal":
this.terminal.clearTerminal();
break;
case "scrollUp":
this.terminal.scrollUp();
break;
case "scrollDown":
this.terminal.scrollDown();
break;
case "showLoading":
case "useRecovery" -> this.useRecovery = true;
case "addLine" -> this.terminal.addLine(arg);
case "setLastLine" -> this.terminal.setLastLine(arg);
case "clearTerminal" -> this.terminal.clearTerminal();
case "scrollUp" -> this.terminal.scrollUp();
case "scrollDown" -> this.terminal.scrollDown();
case "showLoading" -> {
this.isRecoveryBar = false;
if (!arg.isEmpty()) {
try {
@ -627,28 +660,24 @@ public class InstallerActivity extends FoxActivity {
this.progressIndicator.setIndeterminate(true);
}
this.progressIndicator.setVisibility(View.VISIBLE);
break;
case "setLoading":
}
case "setLoading" -> {
this.isRecoveryBar = false;
try {
this.progressIndicator.setProgressCompat(
Short.parseShort(arg), true);
this.progressIndicator.setProgressCompat(Short.parseShort(arg), true);
} catch (Exception ignored) {
}
break;
case "hideLoading":
}
case "hideLoading" -> {
this.isRecoveryBar = false;
this.progressIndicator.setVisibility(View.GONE);
break;
case "setSupportLink":
}
case "setSupportLink" -> {
// Only set link if valid
if (arg.isEmpty() || (arg.startsWith("https://") &&
arg.indexOf('/', 8) > 8))
if (arg.isEmpty() || (arg.startsWith("https://") && arg.indexOf('/', 8) > 8))
this.supportLink = arg;
break;
case "disableANSI":
this.terminal.disableAnsi();
break;
}
case "disableANSI" -> this.terminal.disableAnsi();
}
}
@ -659,8 +688,7 @@ public class InstallerActivity extends FoxActivity {
public void disable() {
this.enabled = false;
if (this.isRecoveryBar) {
UiThreadHandler.runAndWait(() ->
this.processCommand("setLoading 256"));
UiThreadHandler.runAndWait(() -> this.processCommand("setLoading 256"));
}
}
@ -669,14 +697,6 @@ public class InstallerActivity extends FoxActivity {
}
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) return true;
return super.dispatchKeyEvent(event);
}
public static class InstallerMonitor extends CallbackList<String> {
private static final String DEFAULT_ERR = "! Install failed";
private final String installScriptErr;
@ -685,14 +705,12 @@ public class InstallerActivity extends FoxActivity {
public InstallerMonitor(File installScript) {
super(Runnable::run);
this.installScriptErr =
installScript.getAbsolutePath() +
": /data/adb/modules_update/";
this.installScriptErr = installScript.getAbsolutePath() + ": /data/adb/modules_update/";
}
@Override
public void onAddElement(String s) {
Log.d(TAG, "Monitor: " + s);
Timber.i("Monitor: %s", s);
this.lastCommand = s;
}
@ -710,98 +728,15 @@ public class InstallerActivity extends FoxActivity {
String module = installScriptErr.substring(0, i);
SuFile moduleUpdate = new SuFile("/data/adb/modules_update/" + module);
if (moduleUpdate.exists()) {
if (!moduleUpdate.deleteRecursive())
Log.e(TAG, "Failed to delete failed update");
if (!moduleUpdate.deleteRecursive()) Timber.e("Failed to delete failed update");
return "Error: " + installScriptErr.substring(i + 1);
}
} else if (this.forCleanUp != null) {
SuFile moduleUpdate = new SuFile("/data/adb/modules_update/" + this.forCleanUp);
if (moduleUpdate.exists() && !moduleUpdate.deleteRecursive())
Log.e(TAG, "Failed to delete failed update");
Timber.e("Failed to delete failed update");
}
return DEFAULT_ERR;
}
}
private static final HashSet<String> extracted = new HashSet<>();
private File extractInstallScript(String script) {
File compatInstallScript = new File(this.moduleCache, script);
if (!compatInstallScript.exists() || compatInstallScript.length() == 0 ||
!extracted.contains(script)) {
try {
Files.write(compatInstallScript, Files.readAllBytes(
this.getAssets().open(script)));
extracted.add(script);
} catch (IOException e) {
if (compatInstallScript.delete())
extracted.remove(script);
Log.e(TAG, "Failed to extract " + script, e);
return null;
}
}
return compatInstallScript;
}
@SuppressWarnings("SameParameterValue")
private void setInstallStateFinished(boolean success, String message, String optionalLink) {
this.installerTerminal.disableAnsi();
if (success && toDelete != null && !toDelete.delete()) {
SuFile suFile = new SuFile(toDelete.getAbsolutePath());
if (suFile.exists() && !suFile.delete())
Log.w(TAG, "Failed to delete zip file");
else toDelete = null;
} else toDelete = null;
this.runOnUiThread(() -> {
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, 0);
this.setOnBackPressedCallback(null);
this.setDisplayHomeAsUpEnabled(true);
this.progressIndicator.setVisibility(View.GONE);
// This should be improved ?
String reboot_cmd = "/system/bin/svc power reboot || /system/bin/reboot";
this.rebootFloatingButton.setOnClickListener(_view -> {
if (this.warnReboot || MainApplication.shouldPreventReboot()) {
MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(this);
builder
.setTitle(R.string.install_terminal_reboot_now)
.setCancelable(false)
.setIcon(R.drawable.ic_reboot_24)
.setPositiveButton(R.string.yes, (x, y) -> Shell.cmd(reboot_cmd).submit())
.setNegativeButton(R.string.no, (x, y) -> x.dismiss()).show();
} else {
Shell.cmd(reboot_cmd).submit();
}
});
this.rebootFloatingButton.setVisibility(View.VISIBLE);
if (message != null && !message.isEmpty())
this.installerTerminal.addLine(message);
if (optionalLink != null && !optionalLink.isEmpty()) {
this.setActionBarExtraMenuButton(ActionButtonType.supportIconForUrl(optionalLink),
menu -> {
IntentHelper.openUrl(this, optionalLink);
return true;
});
} else if (success) {
final Intent intent = this.getIntent();
final String config = MainApplication.checkSecret(intent) ?
intent.getStringExtra(Constants.EXTRA_INSTALL_CONFIG) : null;
if (config != null && !config.isEmpty()) {
String configPkg = IntentHelper.getPackageOfConfig(config);
try {
XHooks.checkConfigTargetExists(this, configPkg, config);
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, menu -> {
IntentHelper.openConfig(this, config);
return true;
});
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Config package \"" +
configPkg + "\" missing for installer view");
}
}
}
});
}
}

@ -1,7 +1,6 @@
package com.fox2code.mmm.installer;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -9,15 +8,16 @@ import androidx.annotation.Nullable;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.NotificationType;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.io.Files;
import com.topjohnwu.superuser.NoShellException;
import com.topjohnwu.superuser.Shell;
import java.io.File;
import java.util.ArrayList;
import timber.log.Timber;
public class InstallerInitializer extends Shell.Initializer {
private static final String TAG = "InstallerInitializer";
private static final File MAGISK_SBIN =
new File("/sbin/magisk");
private static final File MAGISK_SYSTEM =
@ -30,7 +30,6 @@ public class InstallerInitializer extends Shell.Initializer {
private static int MAGISK_VERSION_CODE;
private static boolean HAS_RAMDISK;
public static final int ERROR_OK = 0;
public static final int ERROR_NO_PATH = 1;
public static final int ERROR_NO_SU = 2;
public static final int ERROR_OTHER = 3;
@ -60,11 +59,19 @@ public class InstallerInitializer extends Shell.Initializer {
return InstallerInitializer.MAGISK_PATH;
}
/**
* Note: All mirrors are read only on latest magisk
*/
public static String peekMirrorPath() {
return InstallerInitializer.MAGISK_PATH == null ? null :
InstallerInitializer.MAGISK_PATH + "/.magisk/mirror";
}
/**
* Note: Used to detect which modules are currently loaded.
* <p>
* For read/write only "/data/adb/modules" should be used
*/
public static String peekModulesPath() {
return InstallerInitializer.MAGISK_PATH == null ? null :
InstallerInitializer.MAGISK_PATH + "/.magisk/modules";
@ -98,10 +105,10 @@ public class InstallerInitializer extends Shell.Initializer {
error = ERROR_NO_PATH;
} catch (NoShellException e) {
error = ERROR_NO_SU;
Log.w(TAG, "Device don't have root!", e);
} catch (Throwable e) {
Timber.w(e);
} catch (Exception e) {
error = ERROR_OTHER;
Log.e(TAG, "Something happened", e);
Timber.e(e);
}
if (forceCheck) {
InstallerInitializer.MAGISK_PATH = MAGISK_PATH;
@ -138,9 +145,9 @@ public class InstallerInitializer extends Shell.Initializer {
return null;
}
MAGISK_PATH = output.size() < 3 ? "" : output.get(2);
Log.d(TAG, "Magisk runtime path: " + MAGISK_PATH);
Timber.i("Magisk runtime path: %s", MAGISK_PATH);
MAGISK_VERSION_CODE = Integer.parseInt(output.get(1));
Log.d(TAG, "Magisk version code: " + MAGISK_VERSION_CODE);
Timber.i("Magisk version code: %s", MAGISK_VERSION_CODE);
if (MAGISK_VERSION_CODE >= Constants.MAGISK_VER_CODE_FLAT_MODULES &&
MAGISK_VERSION_CODE < Constants.MAGISK_VER_CODE_PATH_SUPPORT &&
(MAGISK_PATH.isEmpty() || !new File(MAGISK_PATH).exists())) {
@ -149,7 +156,7 @@ public class InstallerInitializer extends Shell.Initializer {
if (MAGISK_PATH.length() != 0 && Files.existsSU(new File(MAGISK_PATH))) {
InstallerInitializer.MAGISK_PATH = MAGISK_PATH;
} else {
Log.e(TAG, "Failed to get Magisk path (Got " + MAGISK_PATH + ")");
Timber.e("Failed to get Magisk path (Got " + MAGISK_PATH + ")");
MAGISK_PATH = null;
}
InstallerInitializer.MAGISK_VERSION_CODE = MAGISK_VERSION_CODE;

@ -81,6 +81,7 @@ public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.Te
}
}
@SuppressWarnings("unused")
public void removeLastLine() {
synchronized (lock) {
int size = this.terminal.size();
@ -153,6 +154,7 @@ public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.Te
}
}
@SuppressWarnings("ClassCanBeRecord")
private static class ProcessedLine {
public final String line;
public final Spannable spannable;

@ -1,17 +1,17 @@
package com.fox2code.mmm.manager;
import android.util.Log;
import com.fox2code.mmm.markdown.MarkdownUrlLinker;
import com.fox2code.mmm.utils.FastException;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.io.PropUtils;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import timber.log.Timber;
public class LocalModuleInfo extends ModuleInfo {
public String updateVersion;
public long updateVersionCode = Long.MIN_VALUE;
@ -25,7 +25,7 @@ public class LocalModuleInfo extends ModuleInfo {
}
public void checkModuleUpdate() {
if (this.updateJson != null) {
if (this.updateJson != null && (this.flags & FLAG_MM_REMOTE_MODULE) == 0) {
try {
JSONObject jsonUpdate = new JSONObject(new String(Http.doHttpGet(
this.updateJson, true), StandardCharsets.UTF_8));
@ -48,7 +48,7 @@ public class LocalModuleInfo extends ModuleInfo {
this.updateVersion = PropUtils.shortenVersionName(
this.updateVersion.trim(), this.updateVersionCode);
if (this.updateChangeLog.length() > 1000)
this.updateChangeLog = this.updateChangeLog.substring(0, 1000);
this.updateChangeLog = this.updateChangeLog.substring(1000);
this.updateChangeLog = MarkdownUrlLinker.urlLinkify(this.updateChangeLog);
} catch (Exception e) {
this.updateVersion = null;
@ -56,8 +56,7 @@ public class LocalModuleInfo extends ModuleInfo {
this.updateZipUrl = null;
this.updateChangeLog = "";
this.updateChecksum = null;
Log.w("LocalModuleInfo",
"Failed update checking for module: " + this.id, e);
Timber.w(e, "Failed update checking for module: %s", this.id);
}
}
}

@ -1,7 +1,7 @@
package com.fox2code.mmm.manager;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.io.PropUtils;
/**
* Representation of the module.prop
@ -22,10 +22,11 @@ public class ModuleInfo {
public static final int FLAG_METADATA_INVALID = 0x80000000;
public static final int FLAG_CUSTOM_INTERNAL = 0x40000000;
public static final int FLAG_MM_REMOTE_MODULE = 0x20000000;
private static final int FLAG_FENCE = 0x10000000; // Should never be set
// Magisk standard
public final String id;
public String id;
public String name;
public String version;
public long versionCode;
@ -45,6 +46,8 @@ public class ModuleInfo {
public int maxApi;
// Module status (0 if not from Module Manager)
public int flags;
// Module safety (null if not provided)
public boolean safe;
public ModuleInfo(String id) {
this.id = id;
@ -69,6 +72,7 @@ public class ModuleInfo {
this.minApi = moduleInfo.minApi;
this.maxApi = moduleInfo.maxApi;
this.flags = moduleInfo.flags;
this.safe = moduleInfo.safe;
}
public boolean hasFlag(int flag) {

@ -1,55 +1,66 @@
package com.fox2code.mmm.manager;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.NoodleDebug;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.SyncManager;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.realm.ModuleListCache;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import org.matomo.sdk.extra.TrackHelper;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Objects;
public final class ModuleManager extends SyncManager {
private static final String TAG = "ModuleManager";
import io.realm.Realm;
import io.realm.RealmConfiguration;
import timber.log.Timber;
public final class ModuleManager extends SyncManager {
// New method is not really effective, this flag force app to use old method
public static final boolean FORCE_NEED_FALLBACK = true;
private static final int FLAG_MM_INVALID = ModuleInfo.FLAG_METADATA_INVALID;
private static final int FLAG_MM_UNPROCESSED = ModuleInfo.FLAG_CUSTOM_INTERNAL;
private static final int FLAGS_KEEP_INIT = FLAG_MM_UNPROCESSED |
ModuleInfo.FLAGS_MODULE_ACTIVE | ModuleInfo.FLAG_MODULE_UPDATING_ONLY;
private static final int FLAGS_KEEP_INIT = FLAG_MM_UNPROCESSED | ModuleInfo.FLAGS_MODULE_ACTIVE | ModuleInfo.FLAG_MODULE_UPDATING_ONLY;
private static final int FLAGS_RESET_UPDATE = FLAG_MM_INVALID | FLAG_MM_UNPROCESSED;
private static final ModuleManager INSTANCE = new ModuleManager();
private static final int FLAG_MM_REMOTE_MODULE = ModuleInfo.FLAG_MM_REMOTE_MODULE;
private final HashMap<String, LocalModuleInfo> moduleInfos;
private final SharedPreferences bootPrefs;
private int updatableModuleCount = 0;
private static final ModuleManager INSTANCE = new ModuleManager();
private ModuleManager() {
this.moduleInfos = new HashMap<>();
this.bootPrefs = MainApplication.getBootSharedPreferences();
}
public static ModuleManager getINSTANCE() {
return INSTANCE;
}
private ModuleManager() {
this.moduleInfos = new HashMap<>();
this.bootPrefs = MainApplication.getBootSharedPreferences();
public static boolean isModuleActive(String moduleId) {
ModuleInfo moduleInfo = ModuleManager.getINSTANCE().getModules().get(moduleId);
return moduleInfo != null && (moduleInfo.flags & ModuleInfo.FLAGS_MODULE_ACTIVE) != 0;
}
protected void scanInternal(@NonNull UpdateListener updateListener) {
NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug();
noodleDebug.push("Initialize scan");
// if last_shown_setup is not "v2", then refuse to continue
if (!MainApplication.getSharedPreferences("mmm").getString("last_shown_setup", "").equals("v2")) {
return;
}
boolean firstScan = this.bootPrefs.getBoolean("mm_first_scan", true);
SharedPreferences.Editor editor = firstScan ? this.bootPrefs.edit() : null;
for (ModuleInfo v : this.moduleInfos.values()) {
@ -65,19 +76,53 @@ public final class ModuleManager extends SyncManager {
}
String modulesPath = InstallerInitializer.peekModulesPath();
String[] modules = new SuFile("/data/adb/modules").list();
boolean needFallback = FORCE_NEED_FALLBACK ||
modulesPath == null || !new SuFile(modulesPath).exists();
boolean needFallback = FORCE_NEED_FALLBACK || modulesPath == null || !new SuFile(modulesPath).exists();
if (!FORCE_NEED_FALLBACK && needFallback) {
Log.e(TAG, "Failed to detect modules folder, using fallback instead.");
Timber.e("using fallback instead.");
}
noodleDebug.replace("Scan");
if (BuildConfig.DEBUG) Timber.d("Scan");
StringBuilder modulesList = new StringBuilder();
if (modules != null) {
noodleDebug.push("");
for (String module : modules) {
if (!new SuFile("/data/adb/modules/" + module).isDirectory())
continue; // Ignore non directory files inside modules folder
noodleDebug.replace(module);
LocalModuleInfo moduleInfo = moduleInfos.get(module);
// next, merge the module info with a record from ModuleListCache if it exists
RealmConfiguration realmConfiguration;
// get all dirs under the realms/repos/ dir under app's data dir
File cacheRoot = new File(MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/").toURI());
ModuleListCache moduleListCache;
for (File dir : Objects.requireNonNull(cacheRoot.listFiles())) {
if (dir.isDirectory()) {
// if the dir name matches the module name, use it as the cache dir
File tempCacheRoot = new File(dir.toString());
Timber.d("Looking for cache in %s", tempCacheRoot);
realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(tempCacheRoot).build();
Realm realm = Realm.getInstance(realmConfiguration);
Timber.d("Looking for cache for %s out of %d", module, realm.where(ModuleListCache.class).count());
moduleListCache = realm.where(ModuleListCache.class).equalTo("codename", module).findFirst();
if (moduleListCache != null) {
Timber.d("Found cache for %s", module);
// get module info from cache
if (moduleInfo == null) {
moduleInfo = new LocalModuleInfo(module);
}
moduleInfo.name = !Objects.equals(moduleListCache.getName(), "") ? moduleListCache.getName() : module;
moduleInfo.description = !Objects.equals(moduleListCache.getDescription(), "") ? moduleListCache.getDescription() : null;
moduleInfo.author = !Objects.equals(moduleListCache.getAuthor(), "") ? moduleListCache.getAuthor() : null;
moduleInfo.safe = Objects.equals(moduleListCache.isSafe(), true);
moduleInfo.support = !Objects.equals(moduleListCache.getSupport(), "") ? moduleListCache.getSupport() : null;
moduleInfo.donate = !Objects.equals(moduleListCache.getDonate(), "") ? moduleListCache.getDonate() : null;
moduleInfo.flags |= FLAG_MM_REMOTE_MODULE;
moduleInfos.put(module, moduleInfo);
realm.close();
break;
} else {
realm.close();
}
}
}
if (moduleInfo == null) {
moduleInfo = new LocalModuleInfo(module);
moduleInfos.put(module, moduleInfo);
@ -94,8 +139,7 @@ public final class ModuleManager extends SyncManager {
if (new SuFile("/data/adb/modules/" + module + "/remove").exists()) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UNINSTALLING;
}
if ((firstScan && !needFallback && new SuFile(modulesPath, module).exists()) ||
bootPrefs.getBoolean("module_" + moduleInfo.id + "_active", false)) {
if ((firstScan && !needFallback && new SuFile(modulesPath, module).exists()) || bootPrefs.getBoolean("module_" + moduleInfo.id + "_active", false)) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_ACTIVE;
if (firstScan) {
editor.putBoolean("module_" + moduleInfo.id + "_active", true);
@ -103,31 +147,31 @@ public final class ModuleManager extends SyncManager {
} else if (!needFallback) {
moduleInfo.flags &= ~ModuleInfo.FLAG_MODULE_ACTIVE;
}
if ((moduleInfo.flags & ModuleInfo.FLAGS_MODULE_ACTIVE) != 0
&& (new SuFile("/data/adb/modules/" + module + "/system").exists() ||
new SuFile("/data/adb/modules/" + module + "/vendor").exists() ||
new SuFile("/data/adb/modules/" + module + "/zygisk").exists() ||
new SuFile("/data/adb/modules/" + module + "/riru").exists())) {
if ((moduleInfo.flags & ModuleInfo.FLAGS_MODULE_ACTIVE) != 0 && (new SuFile("/data/adb/modules/" + module + "/system").exists() || new SuFile("/data/adb/modules/" + module + "/vendor").exists() || new SuFile("/data/adb/modules/" + module + "/zygisk").exists() || new SuFile("/data/adb/modules/" + module + "/riru").exists())) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT;
}
try {
PropUtils.readProperties(moduleInfo,
"/data/adb/modules/" + module + "/module.prop", true);
PropUtils.readProperties(moduleInfo, "/data/adb/modules/" + module + "/module.prop", true);
} catch (Exception e) {
Log.d(TAG, "Failed to parse metadata!", e);
if (BuildConfig.DEBUG) Timber.d(e);
moduleInfo.flags |= FLAG_MM_INVALID;
}
// append moduleID:moduleName to the list
modulesList.append(moduleInfo.id).append(":").append(moduleInfo.versionCode).append(",");
}
noodleDebug.pop();
}
noodleDebug.replace("Scan update");
if (modulesList.length() > 0) {
modulesList.deleteCharAt(modulesList.length() - 1);
}
// send list to matomo
TrackHelper.track().event("installed_modules", String.valueOf(modulesList)).with(MainApplication.getINSTANCE().getTracker());
if (BuildConfig.DEBUG) Timber.d("Scan update");
String[] modules_update = new SuFile("/data/adb/modules_update").list();
if (modules_update != null) {
noodleDebug.push("");
for (String module : modules_update) {
if (!new SuFile("/data/adb/modules_update/" + module).isDirectory())
continue; // Ignore non directory files inside modules folder
noodleDebug.replace(module);
if (BuildConfig.DEBUG) Timber.d(module);
LocalModuleInfo moduleInfo = moduleInfos.get(module);
if (moduleInfo == null) {
moduleInfo = new LocalModuleInfo(module);
@ -136,28 +180,24 @@ public final class ModuleManager extends SyncManager {
moduleInfo.flags &= ~FLAGS_RESET_UPDATE;
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UPDATING;
try {
PropUtils.readProperties(moduleInfo,
"/data/adb/modules_update/" + module + "/module.prop", true);
PropUtils.readProperties(moduleInfo, "/data/adb/modules_update/" + module + "/module.prop", true);
} catch (Exception e) {
Log.d(TAG, "Failed to parse metadata!", e);
if (BuildConfig.DEBUG) Timber.d(e);
moduleInfo.flags |= FLAG_MM_INVALID;
}
}
noodleDebug.pop();
}
noodleDebug.replace("Finalize scan");
if (BuildConfig.DEBUG) Timber.d("Finalize scan");
this.updatableModuleCount = 0;
Iterator<LocalModuleInfo> moduleInfoIterator =
this.moduleInfos.values().iterator();
noodleDebug.push("");
Iterator<LocalModuleInfo> moduleInfoIterator = this.moduleInfos.values().iterator();
while (moduleInfoIterator.hasNext()) {
LocalModuleInfo moduleInfo = moduleInfoIterator.next();
noodleDebug.replace(moduleInfo.id);
if (BuildConfig.DEBUG) Timber.d(moduleInfo.id);
if ((moduleInfo.flags & FLAG_MM_UNPROCESSED) != 0) {
moduleInfoIterator.remove();
continue; // Don't process fallbacks if unreferenced
}
if (moduleInfo.updateJson != null) {
if (moduleInfo.updateJson != null && (moduleInfo.flags & FLAG_MM_REMOTE_MODULE) == 0) {
this.updatableModuleCount++;
} else {
moduleInfo.updateVersion = null;
@ -166,20 +206,17 @@ public final class ModuleManager extends SyncManager {
moduleInfo.updateChangeLog = "";
}
if (moduleInfo.name == null || (moduleInfo.name.equals(moduleInfo.id))) {
moduleInfo.name = Character.toUpperCase(moduleInfo.id.charAt(0)) +
moduleInfo.id.substring(1).replace('_', ' ');
moduleInfo.name = Character.toUpperCase(moduleInfo.id.charAt(0)) + moduleInfo.id.substring(1).replace('_', ' ');
}
if (moduleInfo.version == null || moduleInfo.version.trim().isEmpty()) {
moduleInfo.version = "v" + moduleInfo.versionCode;
}
moduleInfo.verify();
}
noodleDebug.pop();
if (firstScan) {
editor.putBoolean("mm_first_scan", false);
editor.apply();
}
noodleDebug.pop();
}
public HashMap<String, LocalModuleInfo> getModules() {
@ -230,34 +267,24 @@ public final class ModuleManager extends SyncManager {
public boolean masterClear(ModuleInfo moduleInfo) {
if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT)) return false;
String escapedId = moduleInfo.id.replace("\\", "\\\\")
.replace("\"", "\\\"").replace(" ", "\\ ");
String escapedId = moduleInfo.id.replace("\\", "\\\\").replace("\"", "\\\"").replace(" ", "\\ ");
try { // Check for module that declare having file outside their own folder.
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
SuFileInputStream.open("/data/adb/modules/." + moduleInfo.id + "-files"),
StandardCharsets.UTF_8))) {
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(SuFileInputStream.open("/data/adb/modules/." + moduleInfo.id + "-files"), StandardCharsets.UTF_8))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
line = line.trim().replace(' ', '.');
if (!line.startsWith("/data/adb/") || line.contains("*") ||
line.contains("/../") || line.endsWith("/..") ||
line.startsWith("/data/adb/modules") ||
line.equals("/data/adb/magisk.db")) continue;
line = line.replace("\\", "\\\\")
.replace("\"", "\\\"");
if (!line.startsWith("/data/adb/") || line.contains("*") || line.contains("/../") || line.endsWith("/..") || line.startsWith("/data/adb/modules") || line.equals("/data/adb/magisk.db"))
continue;
line = line.replace("\\", "\\\\").replace("\"", "\\\"");
Shell.cmd("rm -rf \"" + line + "\"").exec();
}
}
} catch (IOException ignored) {}
} catch (IOException ignored) {
}
Shell.cmd("rm -rf /data/adb/modules/" + escapedId + "/").exec();
Shell.cmd("rm -f /data/adb/modules/." + escapedId + "-files").exec();
Shell.cmd("rm -rf /data/adb/modules_update/" + escapedId + "/").exec();
moduleInfo.flags = ModuleInfo.FLAG_METADATA_INVALID;
return true;
}
public static boolean isModuleActive(String moduleId) {
ModuleInfo moduleInfo = ModuleManager.getINSTANCE().getModules().get(moduleId);
return moduleInfo != null && (moduleInfo.flags & ModuleInfo.FLAGS_MODULE_ACTIVE) != 0;
}
}

@ -3,12 +3,9 @@ package com.fox2code.mmm.markdown;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@ -16,37 +13,31 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.XHooks;
import com.fox2code.mmm.utils.BlurUtils;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.io.net.Http;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import org.matomo.sdk.extra.TrackHelper;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import eightbitlab.com.blurview.BlurView;
import eightbitlab.com.blurview.RenderScriptBlur;
import timber.log.Timber;
public class MarkdownActivity extends FoxActivity {
private static final String TAG = "MarkdownActivity";
private static final HashMap<String, String> redirects = new HashMap<>(4);
private static final String[] variants = new String[]{
"readme.md", "README.MD", ".github/README.md"
};
private TextView actionBarPadding;
private ColorDrawable actionBarBackground;
private BlurView actionBarBlur;
private static final String[] variants = new String[]{"readme.md", "README.MD", ".github/README.md"};
private TextView header;
private TextView footer;
@ -59,8 +50,7 @@ public class MarkdownActivity extends FoxActivity {
return Http.doHttpGet(url, true);
} catch (IOException e) {
// Workaround GitHub README.md case sensitivity issue
if (url.startsWith("https://raw.githubusercontent.com/") &&
url.endsWith("/README.md")) {
if (url.startsWith("https://raw.githubusercontent.com/") && url.endsWith("/README.md")) {
String prefix = url.substring(0, url.length() - 9);
for (String suffix : variants) {
newUrl = prefix + suffix;
@ -79,36 +69,27 @@ public class MarkdownActivity extends FoxActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker());
this.setDisplayHomeAsUpEnabled(true);
Intent intent = this.getIntent();
if (!MainApplication.checkSecret(intent)) {
Log.e(TAG, "Impersonation detected!");
Timber.e("Impersonation detected!");
this.forceBackPressed();
return;
}
String url = intent.getExtras()
.getString(Constants.EXTRA_MARKDOWN_URL);
String title = intent.getExtras()
.getString(Constants.EXTRA_MARKDOWN_TITLE);
String config = intent.getExtras()
.getString(Constants.EXTRA_MARKDOWN_CONFIG);
boolean change_boot = intent.getExtras()
.getBoolean(Constants.EXTRA_MARKDOWN_CHANGE_BOOT);
boolean needs_ramdisk = intent.getExtras()
.getBoolean(Constants.EXTRA_MARKDOWN_NEEDS_RAMDISK);
int min_magisk = intent.getExtras()
.getInt(Constants.EXTRA_MARKDOWN_MIN_MAGISK);
int min_api = intent.getExtras()
.getInt(Constants.EXTRA_MARKDOWN_MIN_API);
int max_api = intent.getExtras()
.getInt(Constants.EXTRA_MARKDOWN_MAX_API);
String url = intent.getExtras().getString(Constants.EXTRA_MARKDOWN_URL);
String title = intent.getExtras().getString(Constants.EXTRA_MARKDOWN_TITLE);
String config = intent.getExtras().getString(Constants.EXTRA_MARKDOWN_CONFIG);
boolean change_boot = intent.getExtras().getBoolean(Constants.EXTRA_MARKDOWN_CHANGE_BOOT);
boolean needs_ramdisk = intent.getExtras().getBoolean(Constants.EXTRA_MARKDOWN_NEEDS_RAMDISK);
int min_magisk = intent.getExtras().getInt(Constants.EXTRA_MARKDOWN_MIN_MAGISK);
int min_api = intent.getExtras().getInt(Constants.EXTRA_MARKDOWN_MIN_API);
int max_api = intent.getExtras().getInt(Constants.EXTRA_MARKDOWN_MAX_API);
if (title != null && !title.isEmpty()) {
this.setTitle(title);
}
setActionBarBackground(null);
this.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, 0);
if (config != null && !config.isEmpty()) {
String configPkg = IntentHelper.getPackageOfConfig(config);
try {
@ -118,21 +99,22 @@ public class MarkdownActivity extends FoxActivity {
return true;
});
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Config package \"" +
configPkg + "\" missing for markdown view");
Timber.w("Config package \"" + configPkg + "\" missing for markdown view");
}
}
Log.i(TAG, "Url for markdown " + url);
// validate the url won't crash the app
if (url == null || url.isEmpty() || url.contains("..")) {
Timber.e("Invalid url %s", String.valueOf(url));
this.forceBackPressed();
return;
}
//noinspection UnnecessaryCallToStringValueOf
Timber.i("Url for markdown %s", String.valueOf(url));
setContentView(R.layout.markdown_view);
final ViewGroup markdownBackground = findViewById(R.id.markdownBackground);
final TextView textView = findViewById(R.id.markdownView);
this.actionBarPadding = findViewById(R.id.markdown_action_bar_padding);
this.actionBarBackground = new ColorDrawable(Color.TRANSPARENT);
this.actionBarBlur = findViewById(R.id.markdown_action_bar_blur);
this.header = findViewById(R.id.markdownHeader);
this.footer = findViewById(R.id.markdownFooter);
this.actionBarBlur.setBackground(this.actionBarBackground);
BlurUtils.setupBlur(this.actionBarBlur, this, markdownBackground);
this.updateBlurState();
UiThreadHandler.handler.post(() -> // Fix header/footer height
this.updateScreenInsets(this.getResources().getConfiguration()));
@ -147,67 +129,46 @@ public class MarkdownActivity extends FoxActivity {
new Thread(() -> {
try {
Log.d(TAG, "Downloading");
Timber.i("Downloading");
byte[] rawMarkdown = getRawMarkdown(url);
Log.d(TAG, "Encoding");
Timber.i("Encoding");
String markdown = new String(rawMarkdown, StandardCharsets.UTF_8);
Log.d(TAG, "Done!");
Timber.i("Done!");
runOnUiThread(() -> {
findViewById(R.id.markdownFooter)
.setMinimumHeight(this.getNavigationBarHeight());
MainApplication.getINSTANCE().getMarkwon().setMarkdown(
textView, MarkdownUrlLinker.urlLinkify(markdown));
findViewById(R.id.markdownFooter).setMinimumHeight(this.getNavigationBarHeight());
MainApplication.getINSTANCE().getMarkwon().setMarkdown(textView, MarkdownUrlLinker.urlLinkify(markdown));
if (markdownBackground != null) {
markdownBackground.setClickable(true);
markdownBackground.setOnClickListener(v -> this.onBackPressed());
}
});
} catch (Exception e) {
Log.e(TAG, "Failed download", e);
runOnUiThread(() -> {
Toast.makeText(this, R.string.failed_download,
Toast.LENGTH_SHORT).show();
this.onBackPressed();
});
Timber.e(e);
runOnUiThread(() -> Toast.makeText(this, R.string.failed_download, Toast.LENGTH_SHORT).show());
}
}, "Markdown load thread").start();
}
private void updateBlurState() {
boolean isLightMode = this.isLightTheme();
int colorBackground;
try {
colorBackground = this.getColorCompat(
android.R.attr.windowBackground);
} catch (Resources.NotFoundException e) {
colorBackground = this.getColorCompat(isLightMode ?
R.color.white : R.color.black);
}
if (MainApplication.isBlurEnabled()) {
this.actionBarBlur.setBlurEnabled(true);
this.actionBarBackground.setColor(ColorUtils
.setAlphaComponent(colorBackground, 0x02));
this.actionBarBackground.setColor(Color.TRANSPARENT);
} else {
this.actionBarBlur.setBlurEnabled(false);
this.actionBarBlur.setOverlayColor(Color.TRANSPARENT);
this.actionBarBackground.setColor(colorBackground);
// set bottom navigation bar color to transparent blur
BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation);
bottomNavigationView.setBackgroundColor(Color.TRANSPARENT);
bottomNavigationView.setAlpha(0.8F);
// set dialogs to have transparent blur
getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
}
}
private void updateScreenInsets() {
this.runOnUiThread(() -> this.updateScreenInsets(
this.getResources().getConfiguration()));
this.runOnUiThread(() -> this.updateScreenInsets(this.getResources().getConfiguration()));
}
private void updateScreenInsets(Configuration configuration) {
boolean landscape = configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE;
boolean landscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE;
int bottomInset = (landscape ? 0 : this.getNavigationBarHeight());
int statusBarHeight = getStatusBarHeight();
int actionBarHeight = getActionBarHeight();
int combinedBarsHeight = statusBarHeight + actionBarHeight;
this.actionBarPadding.setMinHeight(combinedBarsHeight);
this.header.setMinHeight(combinedBarsHeight);
this.footer.setMinHeight(bottomInset);
//this.actionBarBlur.invalidate();
@ -226,8 +187,7 @@ public class MarkdownActivity extends FoxActivity {
}
private void addChip(MarkdownChip markdownChip) {
this.makeChip(this.getString(markdownChip.title),
markdownChip.desc == 0 ? null : this.getString(markdownChip.desc));
this.makeChip(this.getString(markdownChip.title), markdownChip.desc == 0 ? null : this.getString(markdownChip.desc));
}
private void addChip(MarkdownChip markdownChip, String extra) {
@ -237,8 +197,7 @@ public class MarkdownActivity extends FoxActivity {
} else {
title = title + " " + extra;
}
this.makeChip(title, markdownChip.desc == 0 ?
null : this.getString(markdownChip.desc));
this.makeChip(title, markdownChip.desc == 0 ? null : this.getString(markdownChip.desc));
}
private void makeChip(String title, String message) {
@ -248,14 +207,9 @@ public class MarkdownActivity extends FoxActivity {
chip.setVisibility(View.VISIBLE);
if (message != null) {
chip.setOnClickListener(_view -> {
MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(this);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder
.setTitle(title)
.setMessage(message)
.setCancelable(true)
.setPositiveButton(R.string.ok, (x, y) -> x.dismiss()).show();
builder.setTitle(title).setMessage(message).setCancelable(true).setPositiveButton(R.string.ok, (x, y) -> x.dismiss()).show();
});
}
@ -263,46 +217,27 @@ public class MarkdownActivity extends FoxActivity {
}
private String parseAndroidVersion(int version) {
switch (version) {
case Build.VERSION_CODES.JELLY_BEAN:
return "4.1 JellyBean";
case Build.VERSION_CODES.JELLY_BEAN_MR1:
return "4.2 JellyBean";
case Build.VERSION_CODES.JELLY_BEAN_MR2:
return "4.3 JellyBean";
case Build.VERSION_CODES.KITKAT:
return "4.4 KitKat";
case Build.VERSION_CODES.KITKAT_WATCH:
return "4.4 KitKat Watch";
case Build.VERSION_CODES.LOLLIPOP:
return "5.0 Lollipop";
case Build.VERSION_CODES.LOLLIPOP_MR1:
return "5.1 Lollipop";
case Build.VERSION_CODES.M:
return "6.0 Marshmallow";
case Build.VERSION_CODES.N:
return "7.0 Nougat";
case Build.VERSION_CODES.N_MR1:
return "7.1 Nougat";
case Build.VERSION_CODES.O:
return "8.0 Oreo";
case Build.VERSION_CODES.O_MR1:
return "8.1 Oreo";
case Build.VERSION_CODES.P:
return "9.0 Pie";
case Build.VERSION_CODES.Q:
return "10 (Q)";
case Build.VERSION_CODES.R:
return "11 (R)";
case Build.VERSION_CODES.S:
return "12 (S)";
case Build.VERSION_CODES.S_V2:
return "12L";
case Build.VERSION_CODES.TIRAMISU:
return "13 Tiramisu";
default:
return "Sdk: " + version;
}
return switch (version) {
case Build.VERSION_CODES.JELLY_BEAN -> "4.1 JellyBean";
case Build.VERSION_CODES.JELLY_BEAN_MR1 -> "4.2 JellyBean";
case Build.VERSION_CODES.JELLY_BEAN_MR2 -> "4.3 JellyBean";
case Build.VERSION_CODES.KITKAT -> "4.4 KitKat";
case Build.VERSION_CODES.KITKAT_WATCH -> "4.4 KitKat Watch";
case Build.VERSION_CODES.LOLLIPOP -> "5.0 Lollipop";
case Build.VERSION_CODES.LOLLIPOP_MR1 -> "5.1 Lollipop";
case Build.VERSION_CODES.M -> "6.0 Marshmallow";
case Build.VERSION_CODES.N -> "7.0 Nougat";
case Build.VERSION_CODES.N_MR1 -> "7.1 Nougat";
case Build.VERSION_CODES.O -> "8.0 Oreo";
case Build.VERSION_CODES.O_MR1 -> "8.1 Oreo";
case Build.VERSION_CODES.P -> "9.0 Pie";
case Build.VERSION_CODES.Q -> "10 (Q)";
case Build.VERSION_CODES.R -> "11 (R)";
case Build.VERSION_CODES.S -> "12 (S)";
case Build.VERSION_CODES.S_V2 -> "12L";
case Build.VERSION_CODES.TIRAMISU -> "13 Tiramisu";
default -> "Sdk: " + version;
};
}
@Override

@ -1,51 +1,45 @@
package com.fox2code.mmm.markdown;
import android.util.Log;
import com.fox2code.mmm.BuildConfig;
import java.util.ArrayList;
public class MarkdownUrlLinker {
private static final String TAG = "MarkdownUrlLinker";
import timber.log.Timber;
public enum MarkdownUrlLinker {
;
public static String urlLinkify(String url) {
int index = url.indexOf("https://");
if (index == -1) return url;
if (index == -1)
return url;
ArrayList<LinkifyTask> linkifyTasks = new ArrayList<>();
int extra = 0;
while (index != -1) {
int end = url.indexOf(' ', index);
end = end == -1 ? url.indexOf('\n', index) :
Math.min(url.indexOf('\n', index), end);
if (end == -1) end = url.length();
if (index == 0 ||
'\n' == url.charAt(index - 1) ||
' ' == url.charAt(index - 1)) {
end = end == -1 ? url.indexOf('\n', index) : Math.min(url.indexOf('\n', index), end);
if (end == -1)
end = url.length();
if (index == 0 || '\n' == url.charAt(index - 1) || ' ' == url.charAt(index - 1)) {
int endDomain = url.indexOf('/', index + 9);
char endCh = url.charAt(end - 1);
if (endDomain != -1 && endDomain < end &&
endCh != '>' && endCh != ')' && endCh != ']') {
if (endDomain != -1 && endDomain < end && endCh != '>' && endCh != ')' && endCh != ']') {
linkifyTasks.add(new LinkifyTask(index, end));
extra += (end - index) + 4;
if (BuildConfig.DEBUG) {
Log.d(TAG, "Linkify url: " + url.substring(index, end));
}
Timber.d("Linkify url: %s", url.substring(end));
}
}
index = url.indexOf("https://", end);
}
if (linkifyTasks.isEmpty()) return url;
if (linkifyTasks.isEmpty())
return url;
LinkifyTask prev = LinkifyTask.NULL;
StringBuilder stringBuilder = new StringBuilder(url.length() + extra);
for (LinkifyTask linkifyTask : linkifyTasks) {
stringBuilder.append(url, prev.end, linkifyTask.start)
.append('[').append(url, linkifyTask.start, linkifyTask.end)
.append("](").append(url, linkifyTask.start, linkifyTask.end).append(')');
stringBuilder.append(url, prev.end, linkifyTask.start).append('[').append(url, linkifyTask.start, linkifyTask.end).append("](").append(url, linkifyTask.start, linkifyTask.end).append(')');
prev = linkifyTask;
}
if (prev.end != url.length()) stringBuilder.append(url, prev.end, url.length());
Log.d(TAG, "Added Markdown link to " + linkifyTasks.size() + " urls");
if (prev.end != url.length())
stringBuilder.append(url, prev.end, url.length());
Timber.i("Added Markdown link to " + linkifyTasks.size() + " urls");
return stringBuilder.toString();
}

@ -4,7 +4,6 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.text.Spanned;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
@ -12,8 +11,8 @@ import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.appcompat.app.AlertDialog;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.FoxDisplay;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.foxcompat.view.FoxDisplay;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.androidacy.AndroidacyUtil;
@ -26,8 +25,14 @@ import com.fox2code.mmm.utils.IntentHelper;
import com.google.android.material.chip.Chip;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.matomo.sdk.extra.TrackHelper;
import java.util.Objects;
import io.noties.markwon.Markwon;
import timber.log.Timber;
@SuppressWarnings("ReplaceNullCheck")
@SuppressLint("UseCompatLoadingForDrawables")
public enum ActionButtonType {
INFO() {
@ -39,65 +44,66 @@ public enum ActionButtonType {
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("view_notes", name).with(MainApplication.getINSTANCE().getTracker());
String notesUrl = moduleHolder.repoModule.notesUrl;
if (AndroidacyUtil.isAndroidacyLink(notesUrl)) {
IntentHelper.openUrlAndroidacy(button.getContext(), notesUrl, false,
moduleHolder.repoModule.moduleInfo.name,
moduleHolder.getMainModuleConfig());
IntentHelper.openUrlAndroidacy(button.getContext(), notesUrl, false, moduleHolder.repoModule.moduleInfo.name, moduleHolder.getMainModuleConfig());
} else {
IntentHelper.openMarkdown(button.getContext(), notesUrl,
moduleHolder.repoModule.moduleInfo.name,
moduleHolder.getMainModuleConfig(),
moduleHolder.repoModule.moduleInfo.changeBoot,
moduleHolder.repoModule.moduleInfo.needRamdisk,
moduleHolder.repoModule.moduleInfo.minMagisk,
moduleHolder.repoModule.moduleInfo.minApi,
moduleHolder.repoModule.moduleInfo.maxApi
);
IntentHelper.openMarkdown(button.getContext(), notesUrl, moduleHolder.repoModule.moduleInfo.name, moduleHolder.getMainModuleConfig(), moduleHolder.repoModule.moduleInfo.changeBoot, moduleHolder.repoModule.moduleInfo.needRamdisk, moduleHolder.repoModule.moduleInfo.minMagisk, moduleHolder.repoModule.moduleInfo.minApi, moduleHolder.repoModule.moduleInfo.maxApi);
}
}
@Override
public boolean doActionLong(Chip button, ModuleHolder moduleHolder) {
Context context = button.getContext();
Toast.makeText(context, context.getString(R.string.module_id_prefix) +
moduleHolder.moduleId, Toast.LENGTH_SHORT).show();
Toast.makeText(context, context.getString(R.string.module_id_prefix) + moduleHolder.moduleId, Toast.LENGTH_SHORT).show();
return true;
}
},
UPDATE_INSTALL() {
}, UPDATE_INSTALL() {
@Override
public void update(Chip button, ModuleHolder moduleHolder) {
int icon = moduleHolder.hasUpdate() ?
R.drawable.ic_baseline_update_24 :
R.drawable.ic_baseline_system_update_24;
button.setChipIcon(button.getContext().getDrawable(icon));
int icon;
if (moduleHolder.hasUpdate()) {
icon = R.drawable.ic_baseline_update_24;
button.setText(R.string.update);
} else if (moduleHolder.moduleInfo != null) {
icon = R.drawable.ic_baseline_refresh_24;
button.setText(R.string.reinstall);
} else {
icon = R.drawable.ic_baseline_system_update_24;
button.setText(R.string.install);
}
button.setChipIcon(button.getContext().getDrawable(icon));
}
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
if (moduleInfo == null) return;
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("view_update_install", name).with(MainApplication.getINSTANCE().getTracker());
String updateZipUrl = moduleHolder.getUpdateZipUrl();
if (updateZipUrl == null) return;
// Androidacy manage the selection between download and install
if (AndroidacyUtil.isAndroidacyLink(updateZipUrl)) {
IntentHelper.openUrlAndroidacy(
button.getContext(), updateZipUrl, true,
moduleInfo.name, moduleInfo.config);
IntentHelper.openUrlAndroidacy(button.getContext(), updateZipUrl, true, moduleInfo.name, moduleInfo.config);
return;
}
boolean hasRoot = InstallerInitializer.peekMagiskPath() != null
&& !MainApplication.isShowcaseMode();
MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(button.getContext());
builder.setTitle(moduleInfo.name).setCancelable(true)
.setIcon(R.drawable.ic_baseline_extension_24);
boolean hasRoot = InstallerInitializer.peekMagiskPath() != null && !MainApplication.isShowcaseMode();
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(button.getContext());
builder.setTitle(moduleInfo.name).setCancelable(true).setIcon(R.drawable.ic_baseline_extension_24);
CharSequence desc = moduleInfo.description;
Markwon markwon = null;
LocalModuleInfo localModuleInfo = moduleHolder.moduleInfo;
@ -112,21 +118,15 @@ public enum ActionButtonType {
} else {
builder.setMessage(desc);
}
Log.d("Test", "URL: " + updateZipUrl);
builder.setNegativeButton(R.string.download_module, (x, y) -> {
IntentHelper.openCustomTab(button.getContext(), updateZipUrl);
});
Timber.i("URL: %s", updateZipUrl);
builder.setNegativeButton(R.string.download_module, (x, y) -> IntentHelper.openCustomTab(button.getContext(), updateZipUrl));
if (hasRoot) {
builder.setPositiveButton(moduleHolder.hasUpdate() ?
R.string.update_module : R.string.install_module, (x, y) -> {
builder.setPositiveButton(moduleHolder.hasUpdate() ? R.string.update_module : R.string.install_module, (x, y) -> {
String updateZipChecksum = moduleHolder.getUpdateZipChecksum();
IntentHelper.openInstaller(button.getContext(), updateZipUrl,
moduleInfo.name, moduleInfo.config, updateZipChecksum,
moduleInfo.mmtReborn);
IntentHelper.openInstaller(button.getContext(), updateZipUrl, moduleInfo.name, moduleInfo.config, updateZipChecksum, moduleInfo.mmtReborn);
});
}
ExternalHelper.INSTANCE.injectButton(builder,
() -> Uri.parse(updateZipUrl), moduleHolder.getUpdateZipRepo());
ExternalHelper.INSTANCE.injectButton(builder, () -> Uri.parse(updateZipUrl), moduleHolder.getUpdateZipRepo());
int dim5dp = FoxDisplay.dpToPixel(5);
builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp);
AlertDialog alertDialog = builder.show();
@ -137,37 +137,34 @@ public enum ActionButtonType {
}
}
if (markwon != null) {
TextView messageView = alertDialog.getWindow()
.findViewById(android.R.id.message);
TextView messageView = Objects.requireNonNull(alertDialog.getWindow()).findViewById(android.R.id.message);
markwon.setParsedMarkdown(messageView, (Spanned) desc);
}
}
},
UNINSTALL() {
}, UNINSTALL() {
@Override
public void update(Chip button, ModuleHolder moduleHolder) {
int icon = moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING) ?
R.drawable.ic_baseline_delete_outline_24 : (
!moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING) ||
moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE)) ?
R.drawable.ic_baseline_delete_24 :
R.drawable.ic_baseline_delete_forever_24;
int icon = moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING) ? R.drawable.ic_baseline_delete_outline_24 : (!moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING) || moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE)) ? R.drawable.ic_baseline_delete_24 : R.drawable.ic_baseline_delete_forever_24;
button.setChipIcon(button.getContext().getDrawable(icon));
button.setText(R.string.uninstall);
}
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
if (!moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE |
ModuleInfo.FLAG_MODULE_UNINSTALLING) &&
moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING)) {
if (!moduleHolder.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE | ModuleInfo.FLAG_MODULE_UNINSTALLING) && moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING)) {
doActionLong(button, moduleHolder);
return;
}
Log.d("ActionButtonType", Integer.toHexString(moduleHolder.moduleInfo.flags));
if (!ModuleManager.getINSTANCE().setUninstallState(moduleHolder.moduleInfo,
!moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING))) {
Log.e("ActionButtonType", "Failed to switch uninstalled state!");
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("uninstall_module", name).with(MainApplication.getINSTANCE().getTracker());
Timber.i(Integer.toHexString(moduleHolder.moduleInfo.flags));
if (!ModuleManager.getINSTANCE().setUninstallState(moduleHolder.moduleInfo, !moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING))) {
Timber.e("Failed to switch uninstalled state!");
}
update(button, moduleHolder);
}
@ -177,23 +174,25 @@ public enum ActionButtonType {
// Actually a module having mount is the only issue when deleting module
if (moduleHolder.moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT))
return false; // We can't trust active flag on first boot
new AlertDialog.Builder(button.getContext()).setTitle(R.string.master_delete)
.setPositiveButton(R.string.master_delete_yes, (v, i) -> {
String moduleId = moduleHolder.moduleInfo.id;
if (!ModuleManager.getINSTANCE().masterClear(moduleHolder.moduleInfo)) {
Toast.makeText(button.getContext(), R.string.master_delete_fail,
Toast.LENGTH_SHORT).show();
} else {
moduleHolder.moduleInfo = null;
FoxActivity.getFoxActivity(button).refreshUI();
Log.e("ActionButtonType", "Cleared: " + moduleId);
}
}).setNegativeButton(R.string.master_delete_no, (v, i) -> {
}).create().show();
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(button.getContext());
builder.setTitle(R.string.master_delete);
builder.setPositiveButton(R.string.master_delete_yes, (dialog, which) -> {
String moduleId = moduleHolder.moduleInfo.id;
if (!ModuleManager.getINSTANCE().masterClear(moduleHolder.moduleInfo)) {
Toast.makeText(button.getContext(), R.string.master_delete_fail, Toast.LENGTH_SHORT).show();
} else {
moduleHolder.moduleInfo = null;
FoxActivity.getFoxActivity(button).refreshUI();
Timber.e("Cleared: %s", moduleId);
}
});
builder.setNegativeButton(R.string.master_delete_no, (v, i) -> {
});
builder.create();
builder.show();
return true;
}
},
CONFIG() {
}, CONFIG() {
@Override
public void update(Chip button, ModuleHolder moduleHolder) {
button.setChipIcon(button.getContext().getDrawable(R.drawable.ic_baseline_app_settings_alt_24));
@ -204,43 +203,112 @@ public enum ActionButtonType {
public void doAction(Chip button, ModuleHolder moduleHolder) {
String config = moduleHolder.getMainModuleConfig();
if (config == null) return;
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("config_module", name).with(MainApplication.getINSTANCE().getTracker());
if (AndroidacyUtil.isAndroidacyLink(config)) {
IntentHelper.openUrlAndroidacy(button.getContext(), config, true);
} else {
IntentHelper.openConfig(button.getContext(), config);
}
}
},
SUPPORT() {
}, SUPPORT() {
@Override
public void update(Chip button, ModuleHolder moduleHolder) {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
button.setChipIcon(button.getContext().getDrawable(
supportIconForUrl(moduleInfo.support)));
button.setChipIcon(button.getContext().getDrawable(supportIconForUrl(moduleInfo.support)));
button.setText(R.string.support);
}
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
IntentHelper.openUrl(button.getContext(),
moduleHolder.getMainModuleInfo().support);
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("support_module", name).with(MainApplication.getINSTANCE().getTracker());
IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().support);
}
},
DONATE() {
}, DONATE() {
@Override
public void update(Chip button, ModuleHolder moduleHolder) {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
button.setChipIcon(button.getContext().getDrawable(
donateIconForUrl(moduleInfo.donate)));
button.setChipIcon(button.getContext().getDrawable(donateIconForUrl(moduleInfo.donate)));
button.setText(R.string.donate);
}
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("donate_module", name).with(MainApplication.getINSTANCE().getTracker());
IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate);
}
}, WARNING() {
@Override
public void update(Chip button, ModuleHolder moduleHolder) {
button.setChipIcon(button.getContext().getDrawable(R.drawable.ic_baseline_warning_24));
button.setText(R.string.warning);
}
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("warning_module", name).with(MainApplication.getINSTANCE().getTracker());
new MaterialAlertDialogBuilder(button.getContext()).setTitle(R.string.warning).setMessage(R.string.warning_message).setPositiveButton(R.string.understand, (v, i) -> {
}).create().show();
}
}, SAFE() {
// SAFE is for modules that the api says are clean. only supported by androidacy currently
@Override
public void update(Chip button, ModuleHolder moduleHolder) {
button.setChipIcon(button.getContext().getDrawable(R.drawable.baseline_verified_user_24));
button.setText(R.string.safe);
}
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
String name;
if (moduleHolder.moduleInfo != null) {
name = moduleHolder.moduleInfo.name;
} else {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("safe_module", name).with(MainApplication.getINSTANCE().getTracker());
new MaterialAlertDialogBuilder(button.getContext()).setTitle(R.string.safe_module).setMessage(R.string.safe_message).setPositiveButton(R.string.understand, (v, i) -> {
}).create().show();
}
};
@DrawableRes
private final int iconId;
ActionButtonType() {
this.iconId = 0;
}
@SuppressWarnings("unused")
ActionButtonType(int iconId) {
this.iconId = iconId;
}
@DrawableRes
public static int supportIconForUrl(String url) {
int icon = R.drawable.ic_baseline_support_24;
@ -248,8 +316,7 @@ public enum ActionButtonType {
return icon;
} else if (url.startsWith("https://t.me/")) {
icon = R.drawable.ic_baseline_telegram_24;
} else if (url.startsWith("https://discord.gg/") ||
url.startsWith("https://discord.com/invite/")) {
} else if (url.startsWith("https://discord.gg/") || url.startsWith("https://discord.com/invite/")) {
icon = R.drawable.ic_baseline_discord_24;
} else if (url.startsWith("https://github.com/")) {
icon = R.drawable.ic_github;
@ -266,28 +333,14 @@ public enum ActionButtonType {
int icon = R.drawable.ic_baseline_monetization_on_24;
if (url == null) {
return icon;
} else if (url.startsWith("https://www.paypal.me/") ||
url.startsWith("https://www.paypal.com/paypalme/") ||
url.startsWith("https://www.paypal.com/donate/")) {
} else if (url.startsWith("https://www.paypal.me/") || url.startsWith("https://www.paypal.com/paypalme/") || url.startsWith("https://www.paypal.com/donate/")) {
icon = R.drawable.ic_baseline_paypal_24;
} else if (url.startsWith("https://patreon.com/") ||
url.startsWith("https://www.patreon.com/")) {
} else if (url.startsWith("https://patreon.com/") || url.startsWith("https://www.patreon.com/")) {
icon = R.drawable.ic_patreon;
}
return icon;
}
@DrawableRes
private final int iconId;
ActionButtonType() {
this.iconId = 0;
}
ActionButtonType(int iconId) {
this.iconId = iconId;
}
public void update(Chip button, ModuleHolder moduleHolder) {
button.setChipIcon(button.getContext().getDrawable(this.iconId));
}

@ -2,7 +2,6 @@ package com.fox2code.mmm.module;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
@ -15,23 +14,25 @@ import com.fox2code.mmm.XHooks;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.io.net.Http;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import timber.log.Timber;
public final class ModuleHolder implements Comparable<ModuleHolder> {
private static final String TAG = "ModuleHolder";
public final String moduleId;
public final NotificationType notificationType;
public final Type separator;
public int footerPx;
public View.OnClickListener onClickListener;
public LocalModuleInfo moduleInfo;
public RepoModule repoModule;
public int filterLevel;
@ -57,7 +58,8 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
this.footerPx = -1;
}
public ModuleHolder(int footerPx,boolean header) {
@SuppressWarnings("unused")
public ModuleHolder(int footerPx, boolean header) {
this.moduleId = "";
this.notificationType = null;
this.separator = null;
@ -70,30 +72,19 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
}
public ModuleInfo getMainModuleInfo() {
return this.repoModule != null && (this.moduleInfo == null ||
this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode)
? this.repoModule.moduleInfo : this.moduleInfo;
return this.repoModule != null && (this.moduleInfo == null || this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.moduleInfo : this.moduleInfo;
}
public String getUpdateZipUrl() {
return this.moduleInfo == null || (this.repoModule != null &&
this.moduleInfo.updateVersionCode <
this.repoModule.moduleInfo.versionCode) ?
this.repoModule.zipUrl : this.moduleInfo.updateZipUrl;
return this.moduleInfo == null || (this.repoModule != null && this.moduleInfo.updateVersionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.zipUrl : this.moduleInfo.updateZipUrl;
}
public String getUpdateZipRepo() {
return this.moduleInfo == null || (this.repoModule != null &&
this.moduleInfo.updateVersionCode <
this.repoModule.moduleInfo.versionCode) ?
this.repoModule.repoData.id : "update_json";
return this.moduleInfo == null || (this.repoModule != null && this.moduleInfo.updateVersionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.repoData.id : "update_json";
}
public String getUpdateZipChecksum() {
return this.moduleInfo == null || (this.repoModule != null &&
this.moduleInfo.updateVersionCode <
this.repoModule.moduleInfo.versionCode) ?
this.repoModule.checksum : this.moduleInfo.updateChecksum;
return this.moduleInfo == null || (this.repoModule != null && this.moduleInfo.updateVersionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.checksum : this.moduleInfo.updateChecksum;
}
public String getMainModuleName() {
@ -119,8 +110,7 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
public String getUpdateTimeText() {
if (this.repoModule == null) return "";
long timeStamp = this.repoModule.lastUpdated;
return timeStamp <= 0 ? "" :
MainApplication.formatTime(timeStamp);
return timeStamp <= 0 ? "" : MainApplication.formatTime(timeStamp);
}
public String getRepoName() {
@ -134,16 +124,25 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
public Type getType() {
if (this.footerPx != -1) {
Timber.i("Module %s is footer", this.moduleId);
return Type.FOOTER;
} else if (this.separator != null) {
Timber.i("Module %s is separator", this.moduleId);
return Type.SEPARATOR;
} else if (this.notificationType != null) {
Timber.i("Module %s is notification", this.moduleId);
return Type.NOTIFICATION;
} else if (this.moduleInfo == null) {
return Type.INSTALLABLE;
} else if (this.moduleInfo.versionCode < this.moduleInfo.updateVersionCode ||
(this.repoModule != null && this.moduleInfo.versionCode <
this.repoModule.moduleInfo.versionCode)) {
} else if (this.moduleInfo.versionCode < this.moduleInfo.updateVersionCode || (this.repoModule != null && this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode)) {
Timber.d("Module %s has update", this.moduleId);
MainApplication.getINSTANCE().modulesHaveUpdates = true;
if (!MainApplication.getINSTANCE().updateModules.contains(this.moduleId)) {
MainApplication.getINSTANCE().updateModules.add(this.moduleId);
MainApplication.getINSTANCE().updateModuleCount++;
}
Timber.d("modulesHaveUpdates = %s, updateModuleCount = %s", MainApplication.getINSTANCE().modulesHaveUpdates, MainApplication.getINSTANCE().updateModuleCount);
Timber.d("Module %s has update", this.moduleId);
return Type.UPDATABLE;
} else {
return Type.INSTALLED;
@ -153,8 +152,7 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
public Type getCompareType(Type type) {
if (this.separator != null) {
return this.separator;
} else if (this.notificationType != null &&
this.notificationType.special) {
} else if (this.notificationType != null && this.notificationType.special) {
return Type.SPECIAL_NOTIFICATIONS;
} else {
return type;
@ -162,24 +160,28 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
}
public boolean shouldRemove() {
return this.notificationType != null ? this.notificationType.shouldRemove() :
this.footerPx == -1 && this.moduleInfo == null &&
(this.repoModule == null || !this.repoModule.repoData.isEnabled() ||
(PropUtils.isLowQualityModule(this.repoModule.moduleInfo) &&
!MainApplication.isDisableLowQualityModuleFilter()));
// okay so this is quite possibly the hackiest fucking piece of code i've ever written
// basically, if we have a repomodule that has moduleinfo but no update, remove it-
if (this.repoModule != null && this.moduleInfo != null && !hasUpdate()) {
return true;
}
return this.notificationType != null ? this.notificationType.shouldRemove() : this.footerPx == -1 && this.moduleInfo == null && (this.repoModule == null || !this.repoModule.repoData.isEnabled() || (PropUtils.isLowQualityModule(this.repoModule.moduleInfo) && !MainApplication.isDisableLowQualityModuleFilter()));
}
public void getButtons(Context context, List<ActionButtonType> buttonTypeList, boolean showcaseMode) {
if (!this.isModuleHolder()) return;
LocalModuleInfo localModuleInfo = this.moduleInfo;
// Add warning button if module id begins with a dot - this is a hidden module which could indicate malware
if (this.moduleId.startsWith(".") || !this.moduleId.matches("^[a-zA-Z][a-zA-Z0-9._-]+$")) {
buttonTypeList.add(ActionButtonType.WARNING);
}
if (localModuleInfo != null && !showcaseMode) {
buttonTypeList.add(ActionButtonType.UNINSTALL);
}
if (this.repoModule != null && this.repoModule.notesUrl != null) {
buttonTypeList.add(ActionButtonType.INFO);
}
if ((this.repoModule != null || (localModuleInfo != null &&
localModuleInfo.updateZipUrl != null))) {
if ((this.repoModule != null || (localModuleInfo != null && localModuleInfo.updateZipUrl != null))) {
buttonTypeList.add(ActionButtonType.UPDATE_INSTALL);
}
String config = this.getMainModuleConfig();
@ -192,8 +194,7 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
XHooks.checkConfigTargetExists(context, pkg, config);
buttonTypeList.add(ActionButtonType.CONFIG);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Config package \"" + pkg +
"\" missing for module \"" + this.moduleId + "\"");
Timber.w("Config package \"" + pkg + "\" missing for module \"" + this.moduleId + "\"");
}
}
}
@ -208,11 +209,15 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
if (moduleInfo.donate != null) {
buttonTypeList.add(ActionButtonType.DONATE);
}
if (moduleInfo.safe) {
buttonTypeList.add(ActionButtonType.SAFE);
} else {
Timber.d("Module %s is not safe", this.moduleId);
}
}
public boolean hasUpdate() {
return this.moduleInfo != null && this.repoModule != null &&
this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode;
return this.moduleInfo != null && this.repoModule != null && this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode;
}
@Override
@ -223,29 +228,29 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
Type selfType = this.getCompareType(selfTypeReal);
Type otherType = o.getCompareType(otherTypeReal);
int compare = selfType.compareTo(otherType);
return compare != 0 ? compare :
selfTypeReal == otherTypeReal ?
selfTypeReal.compare(this, o) :
selfTypeReal.compareTo(otherTypeReal);
return compare != 0 ? compare : selfTypeReal == otherTypeReal ? selfTypeReal.compare(this, o) : selfTypeReal.compareTo(otherTypeReal);
}
@NonNull
@Override
public String toString() {
return "ModuleHolder{" + "moduleId='" + moduleId + '\'' + ", notificationType=" + notificationType + ", separator=" + separator + ", footerPx=" + footerPx + '}';
}
public enum Type implements Comparator<ModuleHolder> {
HEADER(R.string.loading, false, false),
SEPARATOR(R.string.loading, false, false) {
HEADER(R.string.loading, false, false), SEPARATOR(R.string.loading, false, false) {
@Override
@SuppressWarnings("ConstantConditions")
public int compare(ModuleHolder o1, ModuleHolder o2) {
return o1.separator.compareTo(o2.separator);
}
},
NOTIFICATION(R.string.loading, true, false) {
}, NOTIFICATION(R.string.loading, true, false) {
@Override
@SuppressWarnings("ConstantConditions")
public int compare(ModuleHolder o1, ModuleHolder o2) {
return o1.notificationType.compareTo(o2.notificationType);
}
},
UPDATABLE(R.string.updatable, true, true) {
}, UPDATABLE(R.string.updatable, true, true) {
@Override
public int compare(ModuleHolder o1, ModuleHolder o2) {
int cmp = Integer.compare(o1.filterLevel, o2.filterLevel);
@ -256,18 +261,15 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
if (cmp != 0) return cmp;
return o1.getMainModuleName().compareTo(o2.getMainModuleName());
}
},
INSTALLED(R.string.installed, true, true) {
}, INSTALLED(R.string.installed, true, true) {
// get stacktrace for debugging
@Override
public int compare(ModuleHolder o1, ModuleHolder o2) {
int cmp = Integer.compare(o1.filterLevel, o2.filterLevel);
if (cmp != 0) return cmp;
return o1.getMainModuleNameLowercase()
.compareTo(o2.getMainModuleNameLowercase());
return o1.getMainModuleNameLowercase().compareTo(o2.getMainModuleNameLowercase());
}
},
SPECIAL_NOTIFICATIONS(R.string.loading, true, false),
INSTALLABLE(R.string.online_repo, true, true) {
}, SPECIAL_NOTIFICATIONS(R.string.loading, true, false), INSTALLABLE(R.string.online_repo, true, true) {
@Override
public int compare(ModuleHolder o1, ModuleHolder o2) {
int cmp = Integer.compare(o1.filterLevel, o2.filterLevel);
@ -278,8 +280,7 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
if (cmp != 0) return cmp;
return o1.getMainModuleName().compareTo(o2.getMainModuleName());
}
},
FOOTER(R.string.loading, false, false);
}, FOOTER(R.string.loading, false, false);
@StringRes
public final int title;
@ -295,18 +296,15 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
// Note: This method should only be called if both element have the same type
@Override
public int compare(ModuleHolder o1, ModuleHolder o2) {
return 0;
if (o1 == o2) {
return 0;
} else if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
} else {
return o1.moduleId.compareTo(o2.moduleId);
}
}
}
@NonNull
@Override
public String toString() {
return "ModuleHolder{" +
"moduleId='" + moduleId + '\'' +
", notificationType=" + notificationType +
", separator=" + separator +
", footerPx=" + footerPx +
'}';
}
}

@ -20,7 +20,7 @@ import androidx.cardview.widget.CardView;
import androidx.core.graphics.ColorUtils;
import androidx.recyclerview.widget.RecyclerView;
import com.fox2code.foxcompat.FoxDisplay;
import com.fox2code.foxcompat.view.FoxDisplay;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.NotificationType;
import com.fox2code.mmm.R;
@ -30,22 +30,27 @@ import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoModule;
import com.google.android.material.chip.Chip;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.google.android.material.materialswitch.MaterialSwitch;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.util.ArrayList;
import java.util.Objects;
import timber.log.Timber;
public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdapter.ViewHolder> {
private static final boolean DEBUG = false;
public final ArrayList<ModuleHolder> moduleHolders = new ArrayList<>();
private static String formatType(ModuleHolder.Type type) {
return type.name().substring(0, 3) + "_" + type.ordinal();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.module_entry, parent, false);
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.module_entry, parent, false);
return new ViewHolder(view);
}
@ -72,7 +77,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
private final CardView cardView;
private final Chip invalidPropsChip;
private final ImageButton buttonAction;
private final SwitchMaterial switchMaterial;
private final MaterialSwitch switchMaterial;
private final TextView titleText;
private final TextView creditText;
private final TextView descriptionText;
@ -81,9 +86,9 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
private final TextView updateText;
private final Chip[] actionsButtons;
private final ArrayList<ActionButtonType> actionButtonsTypes;
private boolean initState;
public ModuleHolder moduleHolder;
public Drawable background;
private boolean initState;
public ViewHolder(@NonNull View itemView) {
super(itemView);
@ -115,14 +120,16 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
onClickListener.onClick(v);
} else if (moduleHolder.notificationType != null) {
onClickListener = moduleHolder.notificationType.onClickListener;
if (onClickListener != null) onClickListener.onClick(v);
if (onClickListener != null)
onClickListener.onClick(v);
}
}
});
this.buttonAction.setClickable(false);
this.switchMaterial.setEnabled(false);
this.switchMaterial.setOnCheckedChangeListener((v, checked) -> {
if (this.initState) return; // Skip if non user
if (this.initState)
return; // Skip if non user
ModuleHolder moduleHolder = this.moduleHolder;
if (moduleHolder != null && moduleHolder.moduleInfo != null) {
ModuleInfo moduleInfo = moduleHolder.moduleInfo;
@ -136,23 +143,23 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
for (int i = 0; i < this.actionsButtons.length; i++) {
final int index = i;
this.actionsButtons[i].setOnClickListener(v -> {
if (this.initState) return; // Skip if non user
if (this.initState)
return; // Skip if non user
ModuleHolder moduleHolder = this.moduleHolder;
if (index < this.actionButtonsTypes.size() && moduleHolder != null) {
this.actionButtonsTypes.get(index)
.doAction((Chip) v, moduleHolder);
this.actionButtonsTypes.get(index).doAction((Chip) v, moduleHolder);
if (moduleHolder.shouldRemove()) {
this.cardView.setVisibility(View.GONE);
}
}
});
this.actionsButtons[i].setOnLongClickListener(v -> {
if (this.initState) return false; // Skip if non user
if (this.initState)
return false; // Skip if non user
ModuleHolder moduleHolder = this.moduleHolder;
boolean didSomething = false;
if (index < this.actionButtonsTypes.size() && moduleHolder != null) {
didSomething = this.actionButtonsTypes.get(index)
.doActionLong((Chip) v, moduleHolder);
didSomething = this.actionButtonsTypes.get(index).doActionLong((Chip) v, moduleHolder);
if (moduleHolder.shouldRemove()) {
this.cardView.setVisibility(View.GONE);
}
@ -188,8 +195,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
if (localModuleInfo != null) {
localModuleInfo.verify();
this.switchMaterial.setVisibility(View.VISIBLE);
this.switchMaterial.setChecked((localModuleInfo.flags &
ModuleInfo.FLAG_MODULE_DISABLED) == 0);
this.switchMaterial.setChecked((localModuleInfo.flags & ModuleInfo.FLAG_MODULE_DISABLED) == 0);
} else {
this.switchMaterial.setVisibility(View.GONE);
}
@ -201,24 +207,10 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
moduleInfo.verify();
this.titleText.setText(moduleInfo.name);
if (localModuleInfo == null || moduleInfo.versionCode >
localModuleInfo.updateVersionCode) {
this.creditText.setText((localModuleInfo == null ||
Objects.equals(moduleInfo.version, localModuleInfo.version) ?
moduleInfo.version : localModuleInfo.version + " (" +
this.getString(R.string.module_last_update) + " " +
moduleInfo.version + ")") + " " +
this.getString(R.string.module_by) + " " + moduleInfo.author);
if (localModuleInfo == null || moduleInfo.versionCode > localModuleInfo.updateVersionCode) {
this.creditText.setText((localModuleInfo == null || Objects.equals(moduleInfo.version, localModuleInfo.version) ? moduleInfo.version : localModuleInfo.version + " (" + this.getString(R.string.module_last_update) + " " + moduleInfo.version + ")") + " " + this.getString(R.string.module_by) + " " + moduleInfo.author);
} else {
this.creditText.setText(localModuleInfo.version + (
(localModuleInfo.updateVersion != null && (Objects.equals(
localModuleInfo.version, localModuleInfo.updateVersion) ||
Objects.equals(localModuleInfo.version,
localModuleInfo.updateVersion + " (" +
localModuleInfo.updateVersionCode + ")"))) ?
"" : " (" + this.getString(R.string.module_last_update) +
" " + localModuleInfo.updateVersion + ")") + " " +
this.getString(R.string.module_by) + " " + localModuleInfo.author);
this.creditText.setText(localModuleInfo.version + ((localModuleInfo.updateVersion != null && (Objects.equals(localModuleInfo.version, localModuleInfo.updateVersion) || Objects.equals(localModuleInfo.version, localModuleInfo.updateVersion + " (" + localModuleInfo.updateVersionCode + ")"))) ? "" : " (" + this.getString(R.string.module_last_update) + " " + localModuleInfo.updateVersion + ")") + " " + this.getString(R.string.module_by) + " " + localModuleInfo.author);
}
if (moduleInfo.description == null || moduleInfo.description.isEmpty()) {
this.descriptionText.setText(R.string.no_desc_found);
@ -230,16 +222,11 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
if (!updateText.isEmpty()) {
RepoModule repoModule = moduleHolder.repoModule;
this.updateText.setVisibility(View.VISIBLE);
this.updateText.setText(
this.getString(R.string.module_last_update) + " " + updateText + "\n" +
this.getString(R.string.module_repo) + " " + moduleHolder.getRepoName() +
(repoModule.qualityText == 0 ? "" : (
"\n" + this.getString(repoModule.qualityText) +
" " + repoModule.qualityValue)));
this.updateText.setText(this.getString(R.string.module_last_update) + " " + updateText + "\n" + this.getString(R.string.module_repo) + " " + moduleHolder.getRepoName() + (repoModule.qualityText == 0 ? "" : ("\n" + this.getString(repoModule.qualityText) + " " + repoModule.qualityValue)));
} else if (moduleHolder.moduleId.equals("hosts")) {
this.updateText.setVisibility(View.VISIBLE);
this.updateText.setText(R.string.magisk_builtin_module);
} else if (moduleHolder.moduleId.equals("substratum")) {
} else if (moduleHolder.moduleId.contains("substratum")) {
this.updateText.setVisibility(View.VISIBLE);
this.updateText.setText(R.string.substratum_builtin_module);
} else {
@ -248,21 +235,18 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
}
this.actionButtonsTypes.clear();
moduleHolder.getButtons(itemView.getContext(), this.actionButtonsTypes, showCaseMode);
this.switchMaterial.setEnabled(!showCaseMode &&
!moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING));
this.switchMaterial.setEnabled(!showCaseMode && !moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING));
for (int i = 0; i < this.actionsButtons.length; i++) {
Chip imageButton = this.actionsButtons[i];
if (i < this.actionButtonsTypes.size()) {
imageButton.setVisibility(View.VISIBLE);
imageButton.setImportantForAccessibility(
View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
imageButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
ActionButtonType button = this.actionButtonsTypes.get(i);
button.update(imageButton, moduleHolder);
imageButton.setContentDescription(button.name());
} else {
imageButton.setVisibility(View.GONE);
imageButton.setImportantForAccessibility(
View.IMPORTANT_FOR_ACCESSIBILITY_NO);
imageButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
imageButton.setContentDescription(null);
}
}
@ -270,14 +254,12 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
this.moduleOptionsHolder.setVisibility(View.GONE);
this.moduleLayoutHelper.setVisibility(View.GONE);
} else if (this.actionButtonsTypes.size() > 2 || !hasUpdateText) {
this.moduleLayoutHelper.setMinHeight(Math.max(FoxDisplay.dpToPixel(36F),
this.moduleOptionsHolder.getHeight() - FoxDisplay.dpToPixel(14F)));
this.moduleLayoutHelper.setMinHeight(Math.max(FoxDisplay.dpToPixel(36F), this.moduleOptionsHolder.getHeight() - FoxDisplay.dpToPixel(14F)));
} else {
this.moduleLayoutHelper.setMinHeight(FoxDisplay.dpToPixel(4F));
}
this.cardView.setClickable(false);
if (moduleHolder.isModuleHolder() &&
moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_ACTIVE)) {
if (moduleHolder.isModuleHolder() && moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_ACTIVE)) {
this.titleText.setTypeface(Typeface.DEFAULT_BOLD);
} else {
this.titleText.setTypeface(Typeface.DEFAULT);
@ -288,9 +270,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
this.buttonAction.setImageResource(moduleHolder.filterLevel);
this.buttonAction.setBackgroundResource(R.drawable.bg_baseline_circle_24);
} else {
this.buttonAction.setVisibility(
type == ModuleHolder.Type.NOTIFICATION ?
View.VISIBLE : View.GONE);
this.buttonAction.setVisibility(type == ModuleHolder.Type.NOTIFICATION ? View.VISIBLE : View.GONE);
this.buttonAction.setBackground(null);
}
this.switchMaterial.setVisibility(View.GONE);
@ -306,19 +286,17 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
this.actionButtonsTypes.clear();
for (Chip button : this.actionsButtons) {
button.setVisibility(View.GONE);
button.setImportantForAccessibility(
View.IMPORTANT_FOR_ACCESSIBILITY_NO);
button.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
button.setContentDescription(null);
}
if (type == ModuleHolder.Type.NOTIFICATION) {
NotificationType notificationType = moduleHolder.notificationType;
this.titleText.setText(notificationType.textId);
// set title text appearance
this.titleText.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge);
this.buttonAction.setImageResource(notificationType.iconId);
this.cardView.setClickable(
notificationType.onClickListener != null ||
moduleHolder.onClickListener != null);
this.titleText.setTypeface(notificationType.special ?
Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
this.cardView.setClickable(notificationType.onClickListener != null || moduleHolder.onClickListener != null);
this.titleText.setTypeface(notificationType.special ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
} else {
this.cardView.setClickable(moduleHolder.onClickListener != null);
this.titleText.setTypeface(Typeface.DEFAULT);
@ -328,31 +306,26 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
this.titleText.setText(moduleHolder.separator.title);
}
if (DEBUG) {
this.titleText.setText(this.titleText.getText() + " " +
formatType(type) + " " + formatType(vType));
this.titleText.setText(this.titleText.getText() + " " + formatType(type) + " " + formatType(vType));
}
// Coloration system
Drawable drawable = this.cardView.getBackground();
if (drawable != null) this.background = drawable;
if (drawable != null)
this.background = drawable;
this.invalidPropsChip.setVisibility(View.GONE);
if (type.hasBackground) {
if (drawable == null) {
this.cardView.setBackground(this.background);
}
int backgroundAttr = R.attr.colorBackgroundFloating;
int foregroundAttr = R.attr.colorOnBackground;
int backgroundAttr = androidx.appcompat.R.attr.colorBackgroundFloating;
int foregroundAttr = com.google.android.material.R.attr.colorOnBackground;
if (type == ModuleHolder.Type.NOTIFICATION) {
foregroundAttr = moduleHolder.notificationType.foregroundAttr;
backgroundAttr = moduleHolder.notificationType.backgroundAttr;
} else if (type == ModuleHolder.Type.INSTALLED &&
moduleHolder.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
} else if (type == ModuleHolder.Type.INSTALLED && moduleHolder.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
this.invalidPropsChip.setOnClickListener(_view -> {
MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(_view.getContext());
builder.setTitle(R.string.low_quality_module)
.setMessage("Actual description for Low-quality module")
.setCancelable(true)
.setPositiveButton(R.string.ok, (x, y) -> x.dismiss()).show();
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(_view.getContext());
builder.setTitle(R.string.low_quality_module).setMessage(R.string.low_quality_module_desc).setCancelable(true).setPositiveButton(R.string.ok, (x, y) -> x.dismiss()).show();
});
// Backup restore
// foregroundAttr = R.attr.colorOnError;
@ -368,8 +341,12 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
if (bgColor == Color.WHITE) {
bgColor = 0xFFF8F8F8;
}
if (theme.getResources().getBoolean(R.bool.force_transparency)) {
bgColor = ColorUtils.setAlphaComponent(bgColor, 0x80);
// if force_transparency is true or theme is transparent_light, set diff bgColor
// get string value of Theme
String themeName = theme.toString();
if (theme.getResources().getBoolean(R.bool.force_transparency) || themeName.contains("transparent")) {
Timber.d("Theme is transparent, fixing bgColor");
bgColor = ColorUtils.setAlphaComponent(bgColor, 0x70);
}
this.titleText.setTextColor(fgColor);
this.buttonAction.setColorFilter(fgColor);
@ -377,7 +354,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
} else {
Resources.Theme theme = this.titleText.getContext().getTheme();
TypedValue value = new TypedValue();
theme.resolveAttribute(R.attr.colorOnBackground, value, true);
theme.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, value, true);
this.buttonAction.setColorFilter(value.data);
this.titleText.setTextColor(value.data);
this.cardView.setBackground(null);
@ -392,8 +369,4 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
return false;
}
}
private static String formatType(ModuleHolder.Type type) {
return type.name().substring(0, 3) + "_" + type.ordinal();
}
}

@ -2,12 +2,13 @@ package com.fox2code.mmm.module;
import android.app.Activity;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainActivity;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.NotificationType;
import com.fox2code.mmm.installer.InstallerInitializer;
@ -18,14 +19,14 @@ import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.repo.RepoModule;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import timber.log.Timber;
public class ModuleViewListBuilder {
private static final String TAG = "ModuleViewListBuilder";
private static final Runnable RUNNABLE = () -> {};
private final EnumSet<NotificationType> notifications = EnumSet.noneOf(NotificationType.class);
private final HashMap<String, ModuleHolder> mappedModuleHolders = new HashMap<>();
@ -44,10 +45,31 @@ public class ModuleViewListBuilder {
this.activity = activity;
}
private static void notifySizeChanged(ModuleViewAdapter moduleViewAdapter,
int index, int oldLen, int newLen) {
// Timber.i("A: " + index + " " + oldLen + " " + newLen);
if (oldLen == newLen) {
if (newLen != 0)
moduleViewAdapter.notifyItemRangeChanged(index, newLen);
} else if (oldLen < newLen) {
if (oldLen != 0)
moduleViewAdapter.notifyItemRangeChanged(index, oldLen);
moduleViewAdapter.notifyItemRangeInserted(
index + oldLen, newLen - oldLen);
} else {
if (newLen != 0)
moduleViewAdapter.notifyItemRangeChanged(index, newLen);
moduleViewAdapter.notifyItemRangeRemoved(
index + newLen, oldLen - newLen);
}
}
public void addNotification(NotificationType notificationType) {
if (notificationType == null) {
Log.w(TAG, "addNotification(null) called!");
Timber.w("addNotification(null) called!");
return;
} else {
Timber.i("addNotification(%s) called", notificationType);
}
synchronized (this.updateLock) {
this.notifications.add(notificationType);
@ -61,8 +83,10 @@ public class ModuleViewListBuilder {
}
ModuleManager moduleManager = ModuleManager.getINSTANCE();
moduleManager.runAfterScan(() -> {
Log.i(TAG, "A1: " + moduleManager.getModules().size());
Timber.i("A1: %s", moduleManager.getModules().size());
for (LocalModuleInfo moduleInfo : moduleManager.getModules().values()) {
// add the local module to the list in MainActivity
MainActivity.localModuleInfoList.add(moduleInfo);
ModuleHolder moduleHolder = this.mappedModuleHolders.get(moduleInfo.id);
if (moduleHolder == null) {
this.mappedModuleHolders.put(moduleInfo.id,
@ -75,6 +99,9 @@ public class ModuleViewListBuilder {
}
public void appendRemoteModules() {
if (BuildConfig.DEBUG) {
Timber.i("appendRemoteModules() called");
}
synchronized (this.updateLock) {
boolean showIncompatible = MainApplication.isShowIncompatibleModules();
for (ModuleHolder moduleHolder : this.mappedModuleHolders.values()) {
@ -82,10 +109,20 @@ public class ModuleViewListBuilder {
}
RepoManager repoManager = RepoManager.getINSTANCE();
repoManager.runAfterUpdate(() -> {
Log.i(TAG, "A2: " + repoManager.getModules().size());
Timber.i("A2: %s", repoManager.getModules().size());
boolean no32bitSupport = Build.SUPPORTED_32_BIT_ABIS.length == 0;
for (RepoModule repoModule : repoManager.getModules().values()) {
if (!repoModule.repoData.isEnabled()) continue;
// add the remote module to the list in MainActivity
MainActivity.onlineModuleInfoList.add(repoModule);
// if repoData is null, something is wrong
if (repoModule.repoData == null) {
Timber.w("RepoData is null for module %s", repoModule.id);
continue;
}
if (!repoModule.repoData.isEnabled()) {
Timber.i("Repo %s is disabled, skipping module %s", repoModule.repoData.id, repoModule.id);
continue;
}
ModuleInfo moduleInfo = repoModule.moduleInfo;
if (!showIncompatible && (moduleInfo.minApi > Build.VERSION.SDK_INT ||
(moduleInfo.maxApi != 0 && moduleInfo.maxApi < Build.VERSION.SDK_INT) ||
@ -106,12 +143,51 @@ public class ModuleViewListBuilder {
moduleHolder = new ModuleHolder(repoModule.id));
}
moduleHolder.repoModule = repoModule;
// check if local module is installed
// iterate over MainActivity.localModuleInfoList until we hit the module with the same id
for (LocalModuleInfo localModuleInfo : MainActivity.localModuleInfoList) {
if (localModuleInfo.id.equals(repoModule.id)) {
moduleHolder.moduleInfo = localModuleInfo;
break;
}
}
}
});
}
}
public void applyTo(final RecyclerView moduleList,final ModuleViewAdapter moduleViewAdapter) {
public void refreshNotificationsUI(ModuleViewAdapter moduleViewAdapter) {
final int notificationCount = this.notifications.size();
notifySizeChanged(moduleViewAdapter, 0,
notificationCount, notificationCount);
}
private boolean matchFilter(ModuleHolder moduleHolder) {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
String query = this.query;
String idLw = moduleInfo.id.toLowerCase(Locale.ROOT);
String nameLw = moduleInfo.name.toLowerCase(Locale.ROOT);
String authorLw = moduleInfo.author == null ? "" :
moduleInfo.author.toLowerCase(Locale.ROOT);
if (query.isEmpty() || query.equals(idLw) ||
query.equals(nameLw) || query.equals(authorLw)) {
moduleHolder.filterLevel = 0; // Lower = better
return true;
}
if (idLw.contains(query) || nameLw.contains(query)) {
moduleHolder.filterLevel = 1;
return true;
}
if (authorLw.contains(query) || (moduleInfo.description != null &&
moduleInfo.description.toLowerCase(Locale.ROOT).contains(query))) {
moduleHolder.filterLevel = 2;
return true;
}
moduleHolder.filterLevel = 3;
return false;
}
public void applyTo(final RecyclerView moduleList, final ModuleViewAdapter moduleViewAdapter) {
if (this.updating) return;
this.updating = true;
ModuleManager.getINSTANCE().afterScan();
@ -119,13 +195,13 @@ public class ModuleViewListBuilder {
final ArrayList<ModuleHolder> moduleHolders;
final int newNotificationsLen;
final boolean first;
final ModuleHolder[] headerFooter = new ModuleHolder[2];
try {
synchronized (this.updateLock) {
// Build start
moduleHolders = new ArrayList<>(Math.min(64,
this.mappedModuleHolders.size() + 5));
int special = 0;
// add notifications
Iterator<NotificationType> notificationTypeIterator = this.notifications.iterator();
while (notificationTypeIterator.hasNext()) {
NotificationType notificationType = notificationTypeIterator.next();
@ -170,14 +246,14 @@ public class ModuleViewListBuilder {
}
}
}
Collections.sort(moduleHolders, this.moduleSorter);
moduleHolders.sort(this.moduleSorter);
// Header is always first
moduleHolders.add(0, headerFooter[0] =
new ModuleHolder(this.headerPx, true));
//moduleHolders.add(0, headerFooter[0] =
// new ModuleHolder(this.headerPx / 2, true));
// Footer is always last
moduleHolders.add(headerFooter[1] =
new ModuleHolder(this.footerPx, false));
Log.i(TAG, "Got " + moduleHolders.size() + " entries!");
//moduleHolders.add(headerFooter[1] =
// new ModuleHolder(this.footerPx * 2, false));
Timber.i("Got " + moduleHolders.size() + " entries!");
// Build end
}
} finally {
@ -217,10 +293,6 @@ public class ModuleViewListBuilder {
int oldLen = moduleViewAdapter.moduleHolders.size();
moduleViewAdapter.moduleHolders.clear();
moduleViewAdapter.moduleHolders.addAll(moduleHolders);
synchronized (this.updateLock) {
headerFooter[0].footerPx = this.headerPx;
headerFooter[1].footerPx = this.footerPx;
}
if (oldNotificationsLen != newNotificationsLen ||
!oldNotifications.equals(this.notifications)) {
notifySizeChanged(moduleViewAdapter, 0,
@ -242,8 +314,6 @@ public class ModuleViewListBuilder {
if (isTop) moduleList.scrollToPosition(0);
if (isBottom) moduleList.scrollToPosition(newLen);
this.updateInsets = () -> {
headerFooter[0].footerPx = this.headerPx;
headerFooter[1].footerPx = this.footerPx;
notifySizeChanged(moduleViewAdapter, 0, 1, 1);
notifySizeChanged(moduleViewAdapter,
moduleHolders.size(), 1, 1);
@ -251,59 +321,9 @@ public class ModuleViewListBuilder {
});
}
public void refreshNotificationsUI(ModuleViewAdapter moduleViewAdapter) {
final int notificationCount = this.notifications.size();
notifySizeChanged(moduleViewAdapter, 0,
notificationCount, notificationCount);
}
private boolean matchFilter(ModuleHolder moduleHolder) {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
String query = this.query;
String idLw = moduleInfo.id.toLowerCase(Locale.ROOT);
String nameLw = moduleInfo.name.toLowerCase(Locale.ROOT);
String authorLw = moduleInfo.author == null ? "" :
moduleInfo.author.toLowerCase(Locale.ROOT);
if (query.isEmpty() || query.equals(idLw) ||
query.equals(nameLw) || query.equals(authorLw)) {
moduleHolder.filterLevel = 0; // Lower = better
return true;
}
if (idLw.contains(query) || nameLw.contains(query)) {
moduleHolder.filterLevel = 1;
return true;
}
if (authorLw.contains(query) || (moduleInfo.description != null &&
moduleInfo.description.toLowerCase(Locale.ROOT).contains(query))) {
moduleHolder.filterLevel = 2;
return true;
}
moduleHolder.filterLevel = 3;
return false;
}
private static void notifySizeChanged(ModuleViewAdapter moduleViewAdapter,
int index, int oldLen, int newLen) {
// Log.i(TAG, "A: " + index + " " + oldLen + " " + newLen);
if (oldLen == newLen) {
if (newLen != 0)
moduleViewAdapter.notifyItemRangeChanged(index, newLen);
} else if (oldLen < newLen) {
if (oldLen != 0)
moduleViewAdapter.notifyItemRangeChanged(index, oldLen);
moduleViewAdapter.notifyItemRangeInserted(
index + oldLen, newLen - oldLen);
} else {
if (newLen != 0)
moduleViewAdapter.notifyItemRangeChanged(index, newLen);
moduleViewAdapter.notifyItemRangeRemoved(
index + newLen, oldLen - newLen);
}
}
public void setQuery(String query) {
synchronized (this.queryLock) {
Log.i(TAG, "Query " + this.query + " -> " + query);
Timber.i("Query " + this.query + " -> " + query);
this.query = query == null ? "" :
query.trim().toLowerCase(Locale.ROOT);
}
@ -313,7 +333,7 @@ public class ModuleViewListBuilder {
synchronized (this.queryLock) {
String newQuery = query == null ? "" :
query.trim().toLowerCase(Locale.ROOT);
Log.i(TAG, "Query change " + this.query + " -> " + newQuery);
Timber.i("Query change " + this.query + " -> " + newQuery);
if (this.query.equals(newQuery))
return false;
this.query = newQuery;

@ -1,8 +1,6 @@
package com.fox2code.mmm.repo;
import android.content.SharedPreferences;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.io.net.Http;
import org.json.JSONException;
import org.json.JSONObject;
@ -15,8 +13,8 @@ public final class CustomRepoData extends RepoData {
boolean loadedExternal;
String override;
CustomRepoData(String url, File cacheRoot, SharedPreferences cachedPreferences) {
super(url, cacheRoot, cachedPreferences);
CustomRepoData(String url, File cacheRoot) {
super(url, cacheRoot);
}
@Override
@ -30,19 +28,32 @@ public final class CustomRepoData extends RepoData {
this.id : this.override;
}
@Override
public boolean isLimited() {
return true;
}
public void quickPrePopulate() throws IOException, JSONException {
JSONObject jsonObject = new JSONObject(
new String(Http.doHttpGet(this.getUrl(),
false), StandardCharsets.UTF_8));
// make sure there's at least a name and a modules or data object
if (!jsonObject.has("name") || (!jsonObject.has("modules") && !jsonObject.has("data"))) {
throw new IllegalArgumentException("Invalid repo: " + this.getUrl());
}
this.name = jsonObject.getString("name").trim();
this.website = jsonObject.optString("website");
this.support = jsonObject.optString("support");
this.donate = jsonObject.optString("donate");
this.submitModule = jsonObject.optString("submitModule");
}
public Object toJSON() {
try {
return new JSONObject()
.put("id", this.id)
.put("name", this.name)
.put("website", this.website)
.put("support", this.support)
.put("donate", this.donate)
.put("submitModule", this.submitModule);
} catch (JSONException ignored) {
return null;
}
}
}

@ -1,79 +1,162 @@
package com.fox2code.mmm.repo;
import android.content.Context;
import android.content.SharedPreferences;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.io.Hashes;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.realm.ReposList;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import timber.log.Timber;
public class CustomRepoManager {
private static final boolean AUTO_RECOMPILE = true;
public static final int MAX_CUSTOM_REPOS = 5;
private final MainApplication mainApplication;
private static final boolean AUTO_RECOMPILE = true;
private final RepoManager repoManager;
private final String[] customRepos;
private int customReposCount;
boolean dirty;
private int customReposCount;
@SuppressWarnings("unused")
CustomRepoManager(MainApplication mainApplication, RepoManager repoManager) {
this.mainApplication = mainApplication;
this.repoManager = repoManager;
this.customRepos = new String[MAX_CUSTOM_REPOS];
this.customReposCount = 0;
SharedPreferences sharedPreferences = this.getSharedPreferences();
int lastFilled = 0;
for (int i = 0; i < MAX_CUSTOM_REPOS; i++) {
String repo = sharedPreferences.getString("repo_" + i, "");
if (!PropUtils.isNullString(repo) && !RepoManager.isBuiltInRepo(repo)) {
lastFilled = i;
int index = AUTO_RECOMPILE ?
this.customReposCount : i;
this.customRepos[index] = repo;
this.customReposCount++;
((CustomRepoData) this.repoManager.addOrGet(repo))
.override = "custom_repo_" + index;
}
// refuse to load if setup is not complete
if (MainApplication.getSharedPreferences("mmm").getString("last_shown_setup", "").equals("")) {
return;
}
if (AUTO_RECOMPILE && (lastFilled + 1) != this.customReposCount) {
SharedPreferences.Editor editor = sharedPreferences.edit().clear();
for (int i = 0; i < MAX_CUSTOM_REPOS; i++) {
if (this.customRepos[i] != null)
editor.putString("repo_" + i, this.customRepos[i]);
}
editor.apply();
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
if (realm.isInTransaction()) {
realm.commitTransaction();
}
}
private SharedPreferences getSharedPreferences() {
return this.mainApplication.getSharedPreferences(
"mmm_custom_repos", Context.MODE_PRIVATE);
int i = 0;
@SuppressWarnings("MismatchedReadAndWriteOfArray") final int[] lastFilled = {0};
realm.executeTransaction(realm1 -> {
// find all repos that are not built-in
for (ReposList reposList : realm1.where(ReposList.class).notEqualTo("id", "androidacy_repo").and().notEqualTo("id", "magisk_alt_repo").and().notEqualTo("id", "magisk_official_repo").findAll()) {
String repo = reposList.getUrl();
if (!PropUtils.isNullString(repo) && !RepoManager.isBuiltInRepo(repo)) {
lastFilled[0] = i;
int index = AUTO_RECOMPILE ? this.customReposCount : i;
this.customRepos[index] = repo;
this.customReposCount++;
((CustomRepoData) this.repoManager.addOrGet(repo)).override = "custom_repo_" + index;
}
}
});
}
public CustomRepoData addRepo(String repo) {
if (RepoManager.isBuiltInRepo(repo))
throw new IllegalArgumentException("Can't add built-in repo to custom repos");
for (String repoEntry : this.customRepos) {
if (repo.equals(repoEntry))
return (CustomRepoData) this.repoManager.get(repoEntry);
if (repo.equals(repoEntry)) return (CustomRepoData) this.repoManager.get(repoEntry);
}
int i = 0;
while (customRepos[i] != null) i++;
customRepos[i] = repo;
this.getSharedPreferences().edit()
.putString("repo_" + i, repo).apply();
// fetch that sweet sweet json
byte[] json;
try {
json = Http.doHttpGet(repo, false);
} catch (Exception e) {
Timber.e(e, "Failed to fetch json from repo");
return null;
}
// get website, support, donate, submitModule. all optional. name is required.
// parse json
JSONObject jsonObject;
try {
jsonObject = new JSONObject(new String(json));
} catch (Exception e) {
Timber.e(e, "Failed to parse json from repo");
return null;
}
// get name
String name;
try {
name = jsonObject.getString("name");
} catch (Exception e) {
Timber.e(e, "Failed to get name from json");
return null;
}
// get website
String website;
try {
website = jsonObject.getString("website");
} catch (Exception e) {
website = null;
}
// get support
String support;
try {
support = jsonObject.getString("support");
} catch (Exception e) {
support = null;
}
// get donate
String donate;
try {
donate = jsonObject.getString("donate");
} catch (Exception e) {
donate = null;
}
// get submitModule
String submitModule;
try {
submitModule = jsonObject.getString("submitModule");
} catch (Exception e) {
submitModule = null;
}
String id = "repo_" + Hashes.hashSha256(repo.getBytes(StandardCharsets.UTF_8));
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
String finalWebsite = website;
String finalSupport = support;
String finalDonate = donate;
String finalSubmitModule = submitModule;
realm.executeTransaction(realm1 -> {
// find the matching entry for repo_0, repo_1, etc.
ReposList reposList = realm1.where(ReposList.class).equalTo("id", id).findFirst();
if (reposList == null) {
reposList = realm1.createObject(ReposList.class, id);
}
reposList.setUrl(repo);
reposList.setName(name);
reposList.setWebsite(finalWebsite);
reposList.setSupport(finalSupport);
reposList.setDonate(finalDonate);
reposList.setSubmitModule(finalSubmitModule);
reposList.setEnabled(true);
// save the object
realm1.copyToRealmOrUpdate(reposList);
});
customReposCount++;
this.dirty = true;
CustomRepoData customRepoData = (CustomRepoData)
this.repoManager.addOrGet(repo);
customRepoData.override = "custom_repo_" + i;
CustomRepoData customRepoData = (CustomRepoData) this.repoManager.addOrGet(repo);
customRepoData.override = "repo_" + id;
customRepoData.id = id;
customRepoData.website = website;
customRepoData.support = support;
customRepoData.donate = donate;
customRepoData.submitModule = submitModule;
customRepoData.name = name;
// Set the enabled state to true
customRepoData.setEnabled(true);
customRepoData.updateEnabledState();
realm.close();
return customRepoData;
}
public CustomRepoData getRepo(int index) {
if (index >= MAX_CUSTOM_REPOS) return null;
String repo = customRepos[index];
return repo == null ? null :
(CustomRepoData) this.repoManager.get(repo);
public CustomRepoData getRepo(String id) {
return (CustomRepoData) this.repoManager.get(id);
}
public void removeRepo(int index) {
@ -81,22 +164,18 @@ public class CustomRepoManager {
if (oldRepo != null) {
customRepos[index] = null;
customReposCount--;
CustomRepoData customRepoData =
(CustomRepoData) this.repoManager.get(oldRepo);
CustomRepoData customRepoData = (CustomRepoData) this.repoManager.get(oldRepo);
if (customRepoData != null) {
customRepoData.setEnabled(false);
customRepoData.override = null;
}
this.getSharedPreferences().edit()
.remove("repo_" + index).apply();
this.dirty = true;
}
}
public boolean hasRepo(String repo) {
for (String repoEntry : this.customRepos) {
if (repo.equals(repoEntry))
return true;
if (repo.equals(repoEntry)) return true;
}
return false;
}
@ -106,11 +185,9 @@ public class CustomRepoManager {
}
public boolean canAddRepo(String repo) {
if (RepoManager.isBuiltInRepo(repo) ||
this.hasRepo(repo) || !this.canAddRepo())
if (RepoManager.isBuiltInRepo(repo) || this.hasRepo(repo) || !this.canAddRepo())
return false;
return repo.startsWith("https://") &&
repo.indexOf('/', 9) != -1;
return repo.startsWith("https://") && repo.indexOf('/', 9) != -1;
}
public int getRepoCount() {
@ -122,5 +199,4 @@ public class CustomRepoManager {
if (needUpdate) this.dirty = false;
return needUpdate;
}
}

@ -1,18 +1,20 @@
package com.fox2code.mmm.repo;
import android.content.SharedPreferences;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainActivity;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.XRepo;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.realm.ModuleListCache;
import com.fox2code.mmm.utils.realm.ReposList;
import org.json.JSONArray;
import org.json.JSONException;
@ -20,60 +22,138 @@ import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import io.realm.RealmResults;
import timber.log.Timber;
public class RepoData extends XRepo {
private static final String TAG = "RepoData";
public final JSONObject supportedProperties = new JSONObject();
private final Object populateLock = new Object();
public final String url;
public final String id;
public final File cacheRoot;
public final SharedPreferences cachedPreferences;
public final File metaDataCache;
public final HashMap<String, RepoModule> moduleHashMap;
public String url;
public String id;
public File cacheRoot;
public HashMap<String, RepoModule> moduleHashMap;
public JSONObject metaDataCache;
public long lastUpdate;
protected String defaultName, defaultWebsite,
defaultSupport, defaultDonate, defaultSubmitModule;
public String name, website, support, donate, submitModule;
protected String defaultName, defaultWebsite, defaultSupport, defaultDonate, defaultSubmitModule;
// array with module info default values
// supported properties for a module
//id=<string>
//name=<string>
//version=<string>
//versionCode=<int>
//author=<string>
//description=<string>
//minApi=<int>
//maxApi=<int>
//minMagisk=<int>
//needRamdisk=<boolean>
//support=<url>
//donate=<url>
//config=<package>
//changeBoot=<boolean>
//mmtReborn=<boolean>
// extra properties only useful for the database
//repoId=<string>
//installed=<boolean>
//installedVersionCode=<int> (only if installed)
private boolean forceHide, enabled; // Cache for speed
protected RepoData(String url, File cacheRoot, SharedPreferences cachedPreferences) {
public RepoData(String url, File cacheRoot) {
// setup supportedProperties
try {
supportedProperties.put("id", "");
supportedProperties.put("name", "");
supportedProperties.put("version", "");
supportedProperties.put("versionCode", "");
supportedProperties.put("author", "");
supportedProperties.put("description", "");
supportedProperties.put("minApi", "");
supportedProperties.put("maxApi", "");
supportedProperties.put("minMagisk", "");
supportedProperties.put("needRamdisk", "");
supportedProperties.put("support", "");
supportedProperties.put("donate", "");
supportedProperties.put("config", "");
supportedProperties.put("changeBoot", "");
supportedProperties.put("mmtReborn", "");
supportedProperties.put("repoId", "");
supportedProperties.put("installed", "");
supportedProperties.put("installedVersionCode", "");
supportedProperties.put("safe", "");
} catch (JSONException e) {
Timber.e(e, "Error while setting up supportedProperties");
}
this.url = url;
this.id = RepoManager.internalIdOfUrl(url);
this.cacheRoot = cacheRoot;
this.cachedPreferences = cachedPreferences;
this.metaDataCache = new File(cacheRoot, "modules.json");
// metadata cache is a realm database from ModuleListCache
this.metaDataCache = null;
this.moduleHashMap = new HashMap<>();
this.defaultName = url; // Set url as default name
this.forceHide = AppUpdateManager.shouldForceHide(this.id);
this.enabled = (!this.forceHide) && MainApplication.getSharedPreferences()
.getBoolean("pref_" + this.id + "_enabled", this.isEnabledByDefault());
this.defaultWebsite = "https://" + Uri.parse(url).getHost() + "/";
if (!this.cacheRoot.isDirectory()) {
this.cacheRoot.mkdirs();
// this.enable is set from the database
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
ReposList reposList = realm.where(ReposList.class).equalTo("id", this.id).findFirst();
if (reposList == null) {
Timber.d("RepoData for %s not found in database", this.id);
// log every repo in db
Object[] fullList = realm.where(ReposList.class).findAll().toArray();
Timber.d("RepoData: " + this.id + ". repos in database: " + fullList.length);
for (Object repo : fullList) {
ReposList r = (ReposList) repo;
Timber.d("RepoData: " + this.id + ". repo: " + r.getId() + " " + r.getName() + " " + r.getWebsite() + " " + r.getSupport() + " " + r.getDonate() + " " + r.getSubmitModule() + " " + r.isEnabled());
}
} else {
if (this.metaDataCache.exists()) {
this.lastUpdate = metaDataCache.lastModified();
if (this.lastUpdate > System.currentTimeMillis()) {
this.lastUpdate = 0; // Don't allow time travel
}
try {
List<RepoModule> modules = this.populate(new JSONObject(
new String(Files.read(this.metaDataCache), StandardCharsets.UTF_8)));
for (RepoModule repoModule : modules) {
if (!this.tryLoadMetadata(repoModule)) {
repoModule.moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID;
}
}
} catch (Exception e) {
this.metaDataCache.delete();
Timber.d("RepoData for %s found in database", this.id);
}
Timber.d("RepoData: " + this.id + ". record in database: " + (reposList != null ? reposList.toString() : "none"));
this.enabled = (!this.forceHide && reposList != null && reposList.isEnabled());
this.defaultWebsite = "https://" + Uri.parse(url).getHost() + "/";
// open realm database
// load metadata from realm database
if (this.enabled) {
try {
this.metaDataCache = ModuleListCache.getRepoModulesAsJson(this.id);
// log count of modules in the database
Timber.d("RepoData: " + this.id + ". modules in database: " + this.metaDataCache.length());
// load repo metadata from ReposList unless it's a built-in repo
if (RepoManager.isBuiltInRepo(this.id)) {
this.name = this.defaultName;
this.website = this.defaultWebsite;
this.support = this.defaultSupport;
this.donate = this.defaultDonate;
this.submitModule = this.defaultSubmitModule;
} else {
// get everything from ReposList realm database
this.name = Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", this.id).findFirst()).getName();
this.website = Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", this.id).findFirst()).getWebsite();
this.support = Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", this.id).findFirst()).getSupport();
this.donate = Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", this.id).findFirst()).getDonate();
this.submitModule = Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", this.id).findFirst()).getSubmitModule();
}
} catch (Exception e) {
Timber.w("Failed to load repo metadata from database: " + e.getMessage() + ". If this is a first time run, this is normal.");
}
}
realm.close();
}
private static boolean isNonNull(String str) {
return str != null && !str.isEmpty() && !"null".equals(str);
}
protected boolean prepare() {
@ -84,8 +164,10 @@ public class RepoData extends XRepo {
List<RepoModule> newModules = new ArrayList<>();
synchronized (this.populateLock) {
String name = jsonObject.getString("name").trim();
String nameForModules = name.endsWith(" (Official)") ?
name.substring(0, name.length() - 11) : name;
// if Official is present, remove it, or (Official), or [Official]. We don't want to show it in the UI
String nameForModules = name.endsWith(" (Official)") ? name.substring(0, name.length() - 11) : name;
nameForModules = nameForModules.endsWith(" [Official]") ? nameForModules.substring(0, nameForModules.length() - 11) : nameForModules;
nameForModules = nameForModules.contains("Official") ? nameForModules.replace("Official", "").trim() : nameForModules;
long lastUpdate = jsonObject.getLong("last_update");
for (RepoModule repoModule : this.moduleHashMap.values()) {
repoModule.processed = false;
@ -95,9 +177,14 @@ public class RepoData extends XRepo {
for (int i = 0; i < len; i++) {
JSONObject module = array.getJSONObject(i);
String moduleId = module.getString("id");
// Deny remote modules ids shorter than 3 chars or containing null char or space
if (moduleId.length() < 3 || moduleId.indexOf('\0') != -1 ||
moduleId.indexOf(' ') != -1 || "ak3-helper".equals(moduleId)) continue;
// module IDs must match the regex ^[a-zA-Z][a-zA-Z0-9._-]+$ and cannot be empty or null or equal ak3-helper
if (moduleId.isEmpty() || moduleId.equals("ak3-helper") || !moduleId.matches("^[a-zA-Z][a-zA-Z0-9._-]+$")) {
continue;
}
// If module id start with a dot, warn user
if (moduleId.charAt(0) == '.') {
Timber.w("This is not recommended and may indicate an attempt to hide the module");
}
long moduleLastUpdate = module.getLong("last_update");
String moduleNotesUrl = module.getString("notes_url");
String modulePropsUrl = module.getString("prop_url");
@ -105,14 +192,17 @@ public class RepoData extends XRepo {
String moduleChecksum = module.optString("checksum");
String moduleStars = module.optString("stars");
String moduleDownloads = module.optString("downloads");
// if downloads is mull or empty, try to get it from the stats field
if (moduleDownloads.isEmpty() && module.has("stats")) {
moduleDownloads = module.optString("stats");
}
RepoModule repoModule = this.moduleHashMap.get(moduleId);
if (repoModule == null) {
repoModule = new RepoModule(this, moduleId);
this.moduleHashMap.put(moduleId, repoModule);
newModules.add(repoModule);
} else {
if (repoModule.lastUpdated < moduleLastUpdate ||
repoModule.moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
if (repoModule.lastUpdated < moduleLastUpdate || repoModule.moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
newModules.add(repoModule);
}
}
@ -123,16 +213,20 @@ public class RepoData extends XRepo {
repoModule.propUrl = modulePropsUrl;
repoModule.zipUrl = moduleZipUrl;
repoModule.checksum = moduleChecksum;
// safety check must be overridden per repo. only androidacy repo has this flag currently
// repoModule.safe = module.optBoolean("safe", false);
if (!moduleStars.isEmpty()) {
try {
repoModule.qualityValue = Integer.parseInt(moduleStars);
repoModule.qualityText = R.string.module_stars;
} catch (NumberFormatException ignored) {}
} catch (NumberFormatException ignored) {
}
} else if (!moduleDownloads.isEmpty()) {
try {
repoModule.qualityValue = Integer.parseInt(moduleDownloads);
repoModule.qualityText = R.string.module_downloads;
} catch (NumberFormatException ignored) {}
} catch (NumberFormatException ignored) {
}
}
}
// Remove no longer existing modules
@ -140,7 +234,10 @@ public class RepoData extends XRepo {
while (moduleInfoIterator.hasNext()) {
RepoModule repoModule = moduleInfoIterator.next();
if (!repoModule.processed) {
new File(this.cacheRoot, repoModule.id + ".prop").delete();
boolean delete = new File(this.cacheRoot, repoModule.id + ".prop").delete();
if (!delete) {
throw new RuntimeException("Failed to delete module metadata");
}
moduleInfoIterator.remove();
} else {
repoModule.moduleInfo.verify();
@ -162,7 +259,7 @@ public class RepoData extends XRepo {
return BuildConfig.ENABLED_REPOS.contains(this.id);
}
public void storeMetadata(RepoModule repoModule,byte[] data) throws IOException {
public void storeMetadata(RepoModule repoModule, byte[] data) throws IOException {
Files.write(new File(this.cacheRoot, repoModule.id + ".prop"), data);
}
@ -171,16 +268,20 @@ public class RepoData extends XRepo {
if (file.exists()) {
try {
ModuleInfo moduleInfo = repoModule.moduleInfo;
PropUtils.readProperties(moduleInfo, file.getAbsolutePath(),
repoModule.repoName + "/" + moduleInfo.name, false);
PropUtils.readProperties(moduleInfo, file.getAbsolutePath(), repoModule.repoName + "/" + moduleInfo.name, false);
moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID;
if (moduleInfo.version == null) {
moduleInfo.version = "v" + moduleInfo.versionCode;
}
return true;
} catch (Exception ignored) {
file.delete();
boolean delete = file.delete();
if (!delete) {
throw new RuntimeException("Failed to delete invalid metadata file");
}
}
} else {
Timber.d("Metadata file not found for %s", repoModule.id);
}
repoModule.moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID;
return false;
@ -188,77 +289,137 @@ public class RepoData extends XRepo {
@Override
public boolean isEnabled() {
return this.enabled;
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm2 = Realm.getInstance(realmConfiguration2);
AtomicBoolean dbEnabled = new AtomicBoolean(false);
realm2.executeTransaction(realm -> {
ReposList reposList = realm.where(ReposList.class).equalTo("id", this.id).findFirst();
if (reposList != null) {
dbEnabled.set(reposList.isEnabled());
} else {
// should never happen but for safety
dbEnabled.set(false);
}
});
realm2.close();
if (dbEnabled.get()) {
return !this.forceHide;
} else {
return false;
}
}
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled && !this.forceHide;
MainApplication.getSharedPreferences().edit()
.putBoolean("pref_" + this.getPreferenceId() + "_enabled", enabled).apply();
// reposlist realm
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm2 = Realm.getInstance(realmConfiguration2);
realm2.executeTransaction(realm -> {
ReposList reposList = realm.where(ReposList.class).equalTo("id", this.id).findFirst();
if (reposList != null) {
reposList.setEnabled(enabled);
}
});
realm2.close();
}
public void updateEnabledState() {
// Make sure first_launch preference is set to false
if (MainActivity.doSetupNowRunning) {
return;
}
if (this.id == null) {
Timber.e("Repo ID is null");
return;
}
// if repo starts with repo_, it's always enabled bc custom repos can't be disabled without being deleted.
this.forceHide = AppUpdateManager.shouldForceHide(this.id);
this.enabled = (!this.forceHide) && MainApplication.getSharedPreferences()
.getBoolean("pref_" + this.getPreferenceId() + "_enabled", this.isEnabledByDefault());
// reposlist realm
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm2 = Realm.getInstance(realmConfiguration2);
boolean dbEnabled = false;
try {
dbEnabled = Objects.requireNonNull(realm2.where(ReposList.class).equalTo("id", this.id).findFirst()).isEnabled();
} catch (Exception e) {
Timber.e(e, "Error while updating enabled state for repo %s", this.id);
}
realm2.close();
this.enabled = (!this.forceHide) && dbEnabled;
}
public String getUrl() {
return this.url;
}
public boolean isLimited() {
return false;
}
public String getPreferenceId() {
return this.id;
}
private static boolean isNonNull(String str) {
return str != null && !str.isEmpty() && !"null".equals(str);
}
// Repo data info getters
@NonNull
@Override
public String getName() {
if (isNonNull(this.name))
return this.name;
if (this.defaultName != null)
return this.defaultName;
if (isNonNull(this.name)) return this.name;
if (this.defaultName != null) return this.defaultName;
return this.url;
}
@NonNull
public String getWebsite() {
if (isNonNull(this.website))
return this.website;
if (this.defaultWebsite != null)
return this.defaultWebsite;
if (isNonNull(this.website)) return this.website;
if (this.defaultWebsite != null) return this.defaultWebsite;
return this.url;
}
public String getSupport() {
if (isNonNull(this.support))
return this.support;
if (isNonNull(this.support)) return this.support;
return this.defaultSupport;
}
public String getDonate() {
if (isNonNull(this.donate))
return this.donate;
if (isNonNull(this.donate)) return this.donate;
return this.defaultDonate;
}
public String getSubmitModule() {
if (isNonNull(this.submitModule))
return this.submitModule;
if (isNonNull(this.submitModule)) return this.submitModule;
return this.defaultSubmitModule;
}
public final boolean isForceHide() {
return this.forceHide;
}
// should update (lastUpdate > 15 minutes)
public boolean shouldUpdate() {
Timber.d("Repo " + this.id + " should update check called");
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm2 = Realm.getInstance(realmConfiguration2);
ReposList repo = realm2.where(ReposList.class).equalTo("id", this.id).findFirst();
// Make sure ModuleListCache for repoId is not null
File cacheRoot = MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/" + this.id);
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build();
Realm realm = Realm.getInstance(realmConfiguration);
RealmResults<ModuleListCache> moduleListCache = realm.where(ModuleListCache.class).equalTo("repoId", this.id).findAll();
if (repo != null) {
if (repo.getLastUpdate() != 0 && moduleListCache.size() != 0) {
long lastUpdate = repo.getLastUpdate();
long currentTime = System.currentTimeMillis();
long diff = currentTime - lastUpdate;
long diffMinutes = diff / (60 * 1000) % 60;
Timber.d("Repo " + this.id + " updated: " + diffMinutes + " minutes ago");
realm.close();
return diffMinutes > (BuildConfig.DEBUG ? 15 : 30);
} else {
Timber.d("Repo " + this.id + " should update could not find repo in database");
Timber.d("This is probably an error, please report this to the developer");
realm.close();
return true;
}
} else {
realm.close();
}
return true;
}
}

@ -1,22 +1,28 @@
package com.fox2code.mmm.repo;
import android.content.Context;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.SharedPreferences;
import android.util.Log;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainActivity;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.XHooks;
import com.fox2code.mmm.XRepo;
import com.fox2code.mmm.androidacy.AndroidacyRepoData;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Hashes;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.NoodleDebug;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.SyncManager;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.Hashes;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.io.net.Http;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.File;
import java.nio.charset.StandardCharsets;
@ -26,38 +32,60 @@ import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
public final class RepoManager extends SyncManager {
private static final String TAG = "RepoManager";
import timber.log.Timber;
private static final String MAGISK_REPO_MANAGER =
"https://magisk-modules-repo.github.io/submission/modules.json";
public static final String MAGISK_REPO =
"https://raw.githubusercontent.com/Magisk-Modules-Repo/submission/modules/modules.json";
public final class RepoManager extends SyncManager {
public static final String MAGISK_REPO = "https://raw.githubusercontent.com/Magisk-Modules-Repo/submission/modules/modules.json";
public static final String MAGISK_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Repo";
public static final String MAGISK_ALT_REPO =
"https://raw.githubusercontent.com/Magisk-Modules-Alt-Repo/json/main/modules.json";
public static final String MAGISK_ALT_REPO_HOMEPAGE =
"https://github.com/Magisk-Modules-Alt-Repo";
public static final String MAGISK_ALT_REPO_JSDELIVR =
"https://cdn.jsdelivr.net/gh/Magisk-Modules-Alt-Repo/json@main/modules.json";
public static final String ANDROIDACY_MAGISK_REPO_ENDPOINT =
"https://production-api.androidacy.com/magisk/repo";
public static final String ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT =
"https://staging-api.androidacy.com/magisk/repo";
public static final String ANDROIDACY_MAGISK_REPO_HOMEPAGE =
"https://www.androidacy.com/modules-repo";
public static final String DG_MAGISK_REPO =
"https://repo.dergoogler.com/modules.json";
public static final String DG_MAGISK_REPO_GITHUB =
"https://googlers-magisk-repo.github.io/modules.json";
public static final String DG_MAGISK_REPO_GITHUB_RAW =
"https://raw.githubusercontent.com/Googlers-Repo/googlers-repo.github.io/master/modules.json";
public static final String MAGISK_ALT_REPO = "https://raw.githubusercontent.com/Magisk-Modules-Alt-Repo/json/main/modules.json";
public static final String MAGISK_ALT_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Alt-Repo";
public static final String MAGISK_ALT_REPO_JSDELIVR = "https://cdn.jsdelivr.net/gh/Magisk-Modules-Alt-Repo/json@main/modules.json";
public static final String ANDROIDACY_MAGISK_REPO_ENDPOINT = "https://production-api.androidacy.com/magisk/repo";
public static final String ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT = "https://staging-api.androidacy.com/magisk/repo";
public static final String ANDROIDACY_MAGISK_REPO_HOMEPAGE = "https://www.androidacy.com/modules-repo";
private static final String MAGISK_REPO_MANAGER = "https://magisk-modules-repo.github.io/submission/modules.json";
private static final Object lock = new Object();
private static final double STEP1 = 0.1D;
private static final double STEP2 = 0.8D;
private static final double STEP3 = 0.1D;
private static volatile RepoManager INSTANCE;
private final MainApplication mainApplication;
private final LinkedHashMap<String, RepoData> repoData;
private final HashMap<String, RepoModule> modules;
public String repoLastErrorName = null;
private AndroidacyRepoData androidacyRepoData;
private CustomRepoManager customRepoManager;
private boolean initialized;
private boolean repoLastSuccess;
private RepoManager(MainApplication mainApplication) {
INSTANCE = this; // Set early fox XHooks
this.initialized = false;
this.mainApplication = mainApplication;
this.repoData = new LinkedHashMap<>();
this.modules = new HashMap<>();
// refuse to load if setup is not complete
if (MainApplication.getSharedPreferences("mmm").getString("last_shown_setup", "").equals("")) {
return;
}
// We do not have repo list config yet.
this.androidacyRepoData = this.addAndroidacyRepoData();
RepoData altRepo = this.addRepoData(MAGISK_ALT_REPO, "Magisk Modules Alt Repo");
altRepo.defaultWebsite = RepoManager.MAGISK_ALT_REPO_HOMEPAGE;
altRepo.defaultSubmitModule = "https://github.com/Magisk-Modules-Alt-Repo/submission/issues";
this.customRepoManager = new CustomRepoManager(mainApplication, this);
XHooks.onRepoManagerInitialize();
// Populate default cache
boolean x = false;
for (RepoData repoData : this.repoData.values()) {
if (repoData == this.androidacyRepoData) {
if (x) return;
x = true;
}
this.populateDefaultCache(repoData);
}
this.initialized = true;
}
public static RepoManager getINSTANCE() {
if (INSTANCE == null || !INSTANCE.initialized) {
@ -93,55 +121,56 @@ public final class RepoManager extends SyncManager {
return INSTANCE;
}
private final MainApplication mainApplication;
private final LinkedHashMap<String, RepoData> repoData;
private final HashMap<String, RepoModule> modules;
private final AndroidacyRepoData androidacyRepoData;
private final CustomRepoManager customRepoManager;
private boolean initialized;
public static String internalIdOfUrl(String url) {
return switch (url) {
case MAGISK_ALT_REPO, MAGISK_ALT_REPO_JSDELIVR -> "magisk_alt_repo";
case ANDROIDACY_MAGISK_REPO_ENDPOINT, ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT ->
"androidacy_repo";
default -> "repo_" + Hashes.hashSha256(url.getBytes(StandardCharsets.UTF_8));
};
}
private RepoManager(MainApplication mainApplication) {
INSTANCE = this; // Set early fox XHooks
this.initialized = false;
this.mainApplication = mainApplication;
this.repoData = new LinkedHashMap<>();
this.modules = new HashMap<>();
// We do not have repo list config yet.
RepoData altRepo = this.addRepoData(
MAGISK_ALT_REPO, "Magisk Modules Alt Repo");
altRepo.defaultWebsite = RepoManager.MAGISK_ALT_REPO_HOMEPAGE;
altRepo.defaultSubmitModule =
"https://github.com/Magisk-Modules-Alt-Repo/submission/issues";
RepoData dgRepo = this.addRepoData(
DG_MAGISK_REPO_GITHUB_RAW, "Googlers Magisk Repo");
dgRepo.defaultWebsite = "https://dergoogler.com/repo";
this.androidacyRepoData = this.addAndroidacyRepoData();
this.customRepoManager = new CustomRepoManager(mainApplication, this);
XHooks.onRepoManagerInitialize();
// Populate default cache
boolean x = false;
for (RepoData repoData:this.repoData.values()) {
if (repoData == this.androidacyRepoData) {
if (x) return; x = true;
}
this.populateDefaultCache(repoData);
}
this.initialized = true;
static boolean isBuiltInRepo(String repo) {
return switch (repo) {
case RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT, RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT, RepoManager.MAGISK_ALT_REPO, RepoManager.MAGISK_ALT_REPO_JSDELIVR ->
true;
default -> false;
};
}
/**
* Safe way to do {@code RepoManager.getInstance().androidacyRepoData.isEnabled()}
* without initializing RepoManager
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isAndroidacyRepoEnabled() {
return INSTANCE != null && INSTANCE.androidacyRepoData != null && INSTANCE.androidacyRepoData.isEnabled();
}
@SuppressWarnings("StatementWithEmptyBody")
private void populateDefaultCache(RepoData repoData) {
for (RepoModule repoModule:repoData.moduleHashMap.values()) {
// if last_shown_setup is not "v2", them=n refuse to continue
if (!MainApplication.getSharedPreferences("mmm").getString("last_shown_setup", "").equals("v2")) {
return;
}
// make sure repodata is not null
if (repoData == null || repoData.moduleHashMap == null) {
return;
}
for (RepoModule repoModule : repoData.moduleHashMap.values()) {
if (!repoModule.moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
RepoModule registeredRepoModule = this.modules.get(repoModule.id);
if (registeredRepoModule == null) {
this.modules.put(repoModule.id, repoModule);
} else if (repoModule.moduleInfo.versionCode >
registeredRepoModule.moduleInfo.versionCode) {
} else if (AndroidacyRepoData.getInstance().isEnabled() && registeredRepoModule.repoData == this.androidacyRepoData) {
// empty
} else if (AndroidacyRepoData.getInstance().isEnabled() && repoModule.repoData == this.androidacyRepoData) {
this.modules.put(repoModule.id, repoModule);
} else if (repoModule.moduleInfo.versionCode > registeredRepoModule.moduleInfo.versionCode) {
this.modules.put(repoModule.id, repoModule);
}
} else {
Log.e(TAG, "Detected module with invalid metadata: " +
repoModule.repoName + "/" + repoModule.id);
Timber.e("Detected module with invalid metadata: " + repoModule.repoName + "/" + repoModule.id);
}
}
}
@ -159,20 +188,18 @@ public final class RepoManager extends SyncManager {
}
public RepoData addOrGet(String url, String fallBackName) {
if (MAGISK_ALT_REPO_JSDELIVR.equals(url))
url = MAGISK_ALT_REPO;
if (DG_MAGISK_REPO.equals(url) ||
DG_MAGISK_REPO_GITHUB.equals(url))
url = DG_MAGISK_REPO_GITHUB_RAW;
if (MAGISK_ALT_REPO_JSDELIVR.equals(url)) url = MAGISK_ALT_REPO;
RepoData repoData;
synchronized (this.syncLock) {
repoData = this.repoData.get(url);
if (repoData == null) {
if (ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT.equals(url) ||
ANDROIDACY_MAGISK_REPO_ENDPOINT.equals(url)) {
if (this.androidacyRepoData != null)
if (ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT.equals(url) || ANDROIDACY_MAGISK_REPO_ENDPOINT.equals(url)) {
//noinspection ReplaceNullCheck
if (this.androidacyRepoData != null) {
return this.androidacyRepoData;
return this.addAndroidacyRepoData();
} else {
return this.addAndroidacyRepoData();
}
} else {
return this.addRepoData(url, fallBackName);
}
@ -181,98 +208,129 @@ public final class RepoManager extends SyncManager {
return repoData;
}
private boolean repoLastResult = true;
private static final double STEP1 = 0.1D;
private static final double STEP2 = 0.8D;
private static final double STEP3 = 0.1D;
@SuppressWarnings("StatementWithEmptyBody")
@SuppressLint("StringFormatInvalid")
protected void scanInternal(@NonNull UpdateListener updateListener) {
NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug();
noodleDebug.push("Downloading indexes");
// Refuse to start if first_launch is not false in shared preferences
if (MainActivity.doSetupNowRunning) {
return;
}
this.modules.clear();
updateListener.update(0D);
// Using LinkedHashSet to deduplicate Androidacy entry.
RepoData[] repoDatas = new LinkedHashSet<>(
this.repoData.values()).toArray(new RepoData[0]);
RepoData[] repoDatas = new LinkedHashSet<>(this.repoData.values()).toArray(new RepoData[0]);
RepoUpdater[] repoUpdaters = new RepoUpdater[repoDatas.length];
int moduleToUpdate = 0;
noodleDebug.push("");
if (!this.hasConnectivity()) {
updateListener.update(STEP3);
return;
}
for (int i = 0; i < repoDatas.length; i++) {
noodleDebug.replace(repoDatas[i].getName());
moduleToUpdate += (repoUpdaters[i] =
new RepoUpdater(repoDatas[i])).fetchIndex();
if (BuildConfig.DEBUG) Timber.d("Preparing to fetch: %s", repoDatas[i].getName());
moduleToUpdate += (repoUpdaters[i] = new RepoUpdater(repoDatas[i])).fetchIndex();
updateListener.update(STEP1 / repoDatas.length * (i + 1));
}
noodleDebug.pop();
noodleDebug.replace("Updating meta-data");
if (BuildConfig.DEBUG) Timber.d("Updating meta-data");
int updatedModules = 0;
boolean allowLowQualityModules = MainApplication.isDisableLowQualityModuleFilter();
noodleDebug.push("");
for (int i = 0; i < repoUpdaters.length; i++) {
// Check if the repo is enabled
if (!repoUpdaters[i].repoData.isEnabled()) {
if (BuildConfig.DEBUG)
Timber.d("Skipping disabled repo: %s", repoUpdaters[i].repoData.getName());
continue;
}
List<RepoModule> repoModules = repoUpdaters[i].toUpdate();
RepoData repoData = repoDatas[i];
noodleDebug.replace(repoData.getName());
Log.d(TAG, "Registering " + repoData.getName());
noodleDebug.push("");
for (RepoModule repoModule:repoModules) {
noodleDebug.replace(repoModule.id);
if (BuildConfig.DEBUG) Timber.d("Registering %s", repoData.getName());
for (RepoModule repoModule : repoModules) {
try {
if (repoModule.propUrl != null &&
!repoModule.propUrl.isEmpty()) {
repoData.storeMetadata(repoModule,
Http.doHttpGet(repoModule.propUrl, false));
Files.write(new File(repoData.cacheRoot, repoModule.id + ".prop"),
Http.doHttpGet(repoModule.propUrl, false));
if (repoModule.propUrl != null && !repoModule.propUrl.isEmpty()) {
repoData.storeMetadata(repoModule, Http.doHttpGet(repoModule.propUrl, false));
Files.write(new File(repoData.cacheRoot, repoModule.id + ".prop"), Http.doHttpGet(repoModule.propUrl, false));
}
if (repoData.tryLoadMetadata(repoModule) && (allowLowQualityModules ||
!PropUtils.isLowQualityModule(repoModule.moduleInfo))) {
if (repoData.tryLoadMetadata(repoModule) && (allowLowQualityModules || !PropUtils.isLowQualityModule(repoModule.moduleInfo))) {
// Note: registeredRepoModule may not be null if registered by multiple repos
RepoModule registeredRepoModule = this.modules.get(repoModule.id);
if (registeredRepoModule == null) {
this.modules.put(repoModule.id, repoModule);
} else if (repoModule.moduleInfo.versionCode >
registeredRepoModule.moduleInfo.versionCode) {
} else if (AndroidacyRepoData.getInstance().isEnabled() && registeredRepoModule.repoData == this.androidacyRepoData) {
// empty
} else if (AndroidacyRepoData.getInstance().isEnabled() && repoModule.repoData == this.androidacyRepoData) {
this.modules.put(repoModule.id, repoModule);
} else if (repoModule.moduleInfo.versionCode > registeredRepoModule.moduleInfo.versionCode) {
this.modules.put(repoModule.id, repoModule);
}
} else {
repoModule.moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID;
}
} catch (Exception e) {
Log.e(TAG, "Failed to get \"" + repoModule.id + "\" metadata", e);
Timber.e(e);
}
updatedModules++;
updateListener.update(STEP1 + (STEP2 / moduleToUpdate * updatedModules));
updateListener.update(STEP1 + (STEP2 / (moduleToUpdate != 0 ? moduleToUpdate : 1) * updatedModules));
}
noodleDebug.pop();
for (RepoModule repoModule:repoUpdaters[i].toApply()) {
for (RepoModule repoModule : repoUpdaters[i].toApply()) {
if ((repoModule.moduleInfo.flags & ModuleInfo.FLAG_METADATA_INVALID) == 0) {
RepoModule registeredRepoModule = this.modules.get(repoModule.id);
if (registeredRepoModule == null) {
this.modules.put(repoModule.id, repoModule);
} else if (repoModule.moduleInfo.versionCode >
registeredRepoModule.moduleInfo.versionCode) {
} else if (AndroidacyRepoData.getInstance().isEnabled() && registeredRepoModule.repoData == this.androidacyRepoData) {
// empty
} else if (AndroidacyRepoData.getInstance().isEnabled() && repoModule.repoData == this.androidacyRepoData) {
this.modules.put(repoModule.id, repoModule);
} else if (repoModule.moduleInfo.versionCode > registeredRepoModule.moduleInfo.versionCode) {
this.modules.put(repoModule.id, repoModule);
}
}
}
}
noodleDebug.pop();
noodleDebug.replace("Finishing update");
noodleDebug.push("");
boolean hasInternet = false;
for (int i = 0; i < repoDatas.length; i++) {
noodleDebug.replace(repoUpdaters[i].repoData.getName());
hasInternet |= repoUpdaters[i].finish();
updateListener.update(STEP1 + STEP2 + (STEP3 / repoDatas.length * (i + 1)));
if (BuildConfig.DEBUG) Timber.d("Finishing update");
if (hasConnectivity()) {
for (int i = 0; i < repoDatas.length; i++) {
// If repo is not enabled, skip
if (!repoDatas[i].isEnabled()) {
if (BuildConfig.DEBUG)
Timber.d("Skipping " + repoDatas[i].getName() + " because it's disabled");
continue;
}
if (BuildConfig.DEBUG)
Timber.d("Finishing: %s", repoUpdaters[i].repoData.getName());
this.repoLastSuccess = repoUpdaters[i].finish();
if (!this.repoLastSuccess) {
Timber.e("Failed to update %s", repoUpdaters[i].repoData.getName());
// Show snackbar on main looper and add some bottom padding
int finalI = i;
Activity context = MainApplication.getINSTANCE().getLastCompatActivity();
new Handler(Looper.getMainLooper()).post(() -> {
if (context != null) {
// Show material dialogue with the repo name. for androidacy repo, show an option to reset the api key. show a message then a list of errors
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
builder.setTitle(R.string.repo_update_failed);
builder.setMessage(context.getString(R.string.repo_update_failed_message, "- " + repoUpdaters[finalI].repoData.getName()));
builder.setPositiveButton(android.R.string.ok, null);
if (repoUpdaters[finalI].repoData instanceof AndroidacyRepoData) {
builder.setNeutralButton(R.string.reset_api_key, (dialog, which) -> {
SharedPreferences.Editor editor = MainApplication.getSharedPreferences("androidacy").edit();
editor.putString("androidacy_api_key", "");
editor.apply();
Toast.makeText(context, R.string.api_key_removed, Toast.LENGTH_SHORT).show();
});
}
builder.show();
}
});
this.repoLastErrorName = repoUpdaters[i].repoData.getName();
}
updateListener.update(STEP1 + STEP2 + (STEP3 / repoDatas.length * (i + 1)));
}
}
noodleDebug.pop();
Log.i(TAG, "Got " + this.modules.size() + " modules!");
Timber.i("Got " + this.modules.size() + " modules!");
updateListener.update(1D);
this.repoLastResult = hasInternet;
noodleDebug.pop(); // pop "Finishing update"
}
public void updateEnabledStates() {
for (RepoData repoData:this.repoData.values()) {
for (RepoData repoData : this.repoData.values()) {
boolean wasEnabled = repoData.isEnabled();
repoData.updateEnabledState();
if (!wasEnabled && repoData.isEnabled()) {
@ -287,49 +345,13 @@ public final class RepoManager extends SyncManager {
}
public boolean hasConnectivity() {
return this.repoLastResult;
}
public static String internalIdOfUrl(String url) {
switch (url) {
case MAGISK_ALT_REPO:
case MAGISK_ALT_REPO_JSDELIVR:
return "magisk_alt_repo";
case ANDROIDACY_MAGISK_REPO_ENDPOINT:
case ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT:
return "androidacy_repo";
case DG_MAGISK_REPO:
case DG_MAGISK_REPO_GITHUB:
case DG_MAGISK_REPO_GITHUB_RAW:
return "dg_magisk_repo";
default:
return "repo_" + Hashes.hashSha1(
url.getBytes(StandardCharsets.UTF_8));
}
}
static boolean isBuiltInRepo(String repo) {
switch (repo) {
case RepoManager.MAGISK_ALT_REPO:
case RepoManager.MAGISK_ALT_REPO_JSDELIVR:
case RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT:
case RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT:
case RepoManager.DG_MAGISK_REPO:
case RepoManager.DG_MAGISK_REPO_GITHUB:
case RepoManager.DG_MAGISK_REPO_GITHUB_RAW:
return true;
}
return false;
return Http.hasConnectivity();
}
private RepoData addRepoData(String url, String fallBackName) {
String id = internalIdOfUrl(url);
File cacheRoot = new File(this.mainApplication.getCacheDir(), id);
SharedPreferences sharedPreferences = this.mainApplication
.getSharedPreferences("mmm_" + id, Context.MODE_PRIVATE);
RepoData repoData = id.startsWith("repo_") ?
new CustomRepoData(url, cacheRoot, sharedPreferences) :
new RepoData(url, cacheRoot, sharedPreferences);
File cacheRoot = new File(this.mainApplication.getDataDir(), "repos/" + id);
RepoData repoData = id.startsWith("repo_") ? new CustomRepoData(url, cacheRoot) : new RepoData(url, cacheRoot);
if (fallBackName != null && !fallBackName.isEmpty()) {
repoData.defaultName = fallBackName;
if (repoData instanceof CustomRepoData) {
@ -339,10 +361,7 @@ public final class RepoManager extends SyncManager {
}
}
switch (url) {
case MAGISK_REPO:
case MAGISK_REPO_MANAGER: {
repoData.defaultWebsite = MAGISK_REPO_HOMEPAGE;
}
case MAGISK_REPO, MAGISK_REPO_MANAGER -> repoData.defaultWebsite = MAGISK_REPO_HOMEPAGE;
}
this.repoData.put(url, repoData);
if (this.initialized) {
@ -352,11 +371,9 @@ public final class RepoManager extends SyncManager {
}
private AndroidacyRepoData addAndroidacyRepoData() {
File cacheRoot = new File(this.mainApplication.getCacheDir(), "androidacy_repo");
SharedPreferences sharedPreferences = this.mainApplication
.getSharedPreferences("mmm_androidacy_repo", Context.MODE_PRIVATE);
AndroidacyRepoData repoData = new AndroidacyRepoData(cacheRoot,
sharedPreferences, MainApplication.isAndroidacyTestMode());
// cache dir is actually under app data
File cacheRoot = this.mainApplication.getDataDirWithPath("realms/repos/androidacy_repo");
AndroidacyRepoData repoData = new AndroidacyRepoData(cacheRoot, MainApplication.isAndroidacyTestMode());
this.repoData.put(ANDROIDACY_MAGISK_REPO_ENDPOINT, repoData);
this.repoData.put(ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT, repoData);
return repoData;
@ -374,12 +391,7 @@ public final class RepoManager extends SyncManager {
return new LinkedHashSet<>(this.repoData.values());
}
/**
* Safe way to do {@code RepoManager.getInstance().androidacyRepoData.isEnabled()}
* without initializing RepoManager
*/
public static boolean isAndroidacyRepoEnabled() {
return INSTANCE != null && INSTANCE.androidacyRepoData != null &&
INSTANCE.androidacyRepoData.isEnabled();
public boolean isLastUpdateSuccess() {
return this.repoLastSuccess;
}
}

@ -18,6 +18,7 @@ public class RepoModule {
@StringRes
public int qualityText;
public int qualityValue;
public boolean safe;
public RepoModule(RepoData repoData, String id) {
this.repoData = repoData;
@ -25,5 +26,24 @@ public class RepoModule {
this.id = id;
this.moduleInfo.flags |=
ModuleInfo.FLAG_METADATA_INVALID;
this.safe = this.moduleInfo.safe;
}
// allows all fields to be set-
public RepoModule(RepoData repoData, String id, String name, String description, String author, String donate, String config, String support, String version, int versionCode) {
this.repoData = repoData;
this.moduleInfo = new ModuleInfo(id);
this.id = id;
this.moduleInfo.name = name;
this.moduleInfo.description = description;
this.moduleInfo.author = author;
this.moduleInfo.donate = donate;
this.moduleInfo.config = config;
this.moduleInfo.support = support;
this.moduleInfo.version = version;
this.moduleInfo.versionCode = versionCode;
this.moduleInfo.flags |=
ModuleInfo.FLAG_METADATA_INVALID;
this.safe = this.moduleInfo.safe;
}
}

@ -1,21 +1,28 @@
package com.fox2code.mmm.repo;
import android.util.Log;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.realm.ModuleListCache;
import com.fox2code.mmm.utils.realm.ReposList;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import io.realm.RealmResults;
import timber.log.Timber;
public class RepoUpdater {
private static final String TAG = "RepoUpdater";
public final RepoData repoData;
public byte[] indexRaw;
private List<RepoModule> toUpdate;
@ -26,12 +33,51 @@ public class RepoUpdater {
}
public int fetchIndex() {
if (!RepoManager.getINSTANCE().hasConnectivity()) {
this.indexRaw = null;
this.toUpdate = Collections.emptyList();
this.toApply = Collections.emptySet();
return 0;
}
if (!this.repoData.isEnabled()) {
this.indexRaw = null;
this.toUpdate = Collections.emptyList();
this.toApply = Collections.emptySet();
return 0;
}
// if we shouldn't update, get the values from the ModuleListCache realm
if (!this.repoData.shouldUpdate() && Objects.equals(this.repoData.id, "androidacy_repo")) { // for now, only enable cache reading for androidacy repo, until we handle storing module prop file values in cache
Timber.d("Fetching index from cache for %s", this.repoData.id);
File cacheRoot = MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/" + this.repoData.id);
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build();
Realm realm = Realm.getInstance(realmConfiguration);
RealmResults<ModuleListCache> results = realm.where(ModuleListCache.class).equalTo("repoId", this.repoData.id).findAll();
// repos-list realm
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm2 = Realm.getInstance(realmConfiguration2);
this.toUpdate = Collections.emptyList();
this.toApply = new HashSet<>();
for (ModuleListCache moduleListCache : results) {
this.toApply.add(new RepoModule(this.repoData, moduleListCache.getCodename(), moduleListCache.getName(), moduleListCache.getDescription(), moduleListCache.getAuthor(), moduleListCache.getDonate(), moduleListCache.getConfig(), moduleListCache.getSupport(), moduleListCache.getVersion(), moduleListCache.getVersionCode()));
}
Timber.d("Fetched %d modules from cache for %s, from %s records", this.toApply.size(), this.repoData.id, results.size());
// apply the toApply list to the toUpdate list
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("modules", new JSONArray(results.asJSON()));
this.toUpdate = this.repoData.populate(jsonObject);
} catch (Exception e) {
Timber.e(e);
}
// close realm
realm.close();
realm2.close();
// Since we reuse instances this should work
this.toApply = new HashSet<>(this.repoData.moduleHashMap.values());
this.toApply.removeAll(this.toUpdate);
// Return repo to update
return this.toUpdate.size();
}
try {
if (!this.repoData.prepare()) {
this.indexRaw = null;
@ -40,15 +86,15 @@ public class RepoUpdater {
return 0;
}
this.indexRaw = Http.doHttpGet(this.repoData.getUrl(), false);
this.toUpdate = this.repoData.populate(new JSONObject(
new String(this.indexRaw, StandardCharsets.UTF_8)));
this.toUpdate = this.repoData.populate(new JSONObject(new String(this.indexRaw, StandardCharsets.UTF_8)));
// Since we reuse instances this should work
this.toApply = new HashSet<>(this.repoData.moduleHashMap.values());
this.toApply.removeAll(this.toUpdate);
// Return repo to update
return this.toUpdate.size();
} catch (Exception e) {
Log.e(TAG, "Failed to get manifest of " + this.repoData.id, e);
} catch (
Exception e) {
Timber.e(e);
this.indexRaw = null;
this.toUpdate = Collections.emptyList();
this.toApply = Collections.emptySet();
@ -65,17 +111,254 @@ public class RepoUpdater {
}
public boolean finish() {
final boolean success = this.indexRaw != null;
var success = new AtomicBoolean(false);
// If repo is not enabled we don't need to do anything, just return true
if (!this.repoData.isEnabled()) {
return true;
}
if (this.indexRaw != null) {
try {
Files.write(this.repoData.metaDataCache, this.indexRaw);
} catch (IOException e) {
e.printStackTrace();
// iterate over modules, using this.supportedProperties as a template to attempt to get each property from the module. everything that is not null is added to the module
// use realm to insert to
// props avail:
File cacheRoot = MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/" + this.repoData.id);
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build();
// array with module info default values
// supported properties for a module
//id=<string>
//name=<string>
//version=<string>
//versionCode=<int>
//author=<string>
//description=<string>
//minApi=<int>
//maxApi=<int>
//minMagisk=<int>
//needRamdisk=<boolean>
//support=<url>
//donate=<url>
//config=<package>
//changeBoot=<boolean>
//mmtReborn=<boolean>
// extra properties only useful for the database
//repoId=<string>
//installed=<boolean>
//installedVersionCode=<int> (only if installed)
//
// all except first six can be null
// this.indexRaw is the raw index file (json)
JSONObject modules = new JSONObject(new String(this.indexRaw, StandardCharsets.UTF_8));
JSONArray modulesArray;
// androidacy repo uses "data" key, others should use "modules" key. Both are JSONArrays
if (this.repoData.getName().equals("Androidacy Modules Repo")) {
// get modules from "data" key. This is a JSONArray so we need to convert it to a JSONObject
modulesArray = modules.getJSONArray("data");
} else {
// get modules from "modules" key. This is a JSONArray so we need to convert it to a JSONObject
modulesArray = modules.getJSONArray("modules");
}
Realm realm = Realm.getInstance(realmConfiguration);
// drop old data
if (realm.isInTransaction()) {
realm.commitTransaction();
}
realm.beginTransaction();
realm.where(ModuleListCache.class).equalTo("repoId", this.repoData.id).findAll().deleteAllFromRealm();
realm.commitTransaction();
// iterate over modules. pls don't hate me for this, its ugly but it works
for (int n = 0; n < modulesArray.length(); n++) {
// get module
JSONObject module = modulesArray.getJSONObject(n);
try {
// get module id
// if codename is present, prefer that over id
String id;
if (module.has("codename") && !module.getString("codename").equals("")) {
id = module.getString("codename");
} else {
id = module.getString("id");
}
// get module name
String name = module.getString("name");
// get module version
String version = module.getString("version");
// get module version code
int versionCode = module.getInt("versionCode");
// get module author
String author = module.getString("author");
// get module description
String description = module.getString("description");
// get module min api
String minApi;
if (module.has("minApi") && !module.getString("minApi").equals("")) {
minApi = module.getString("minApi");
} else {
minApi = "0";
}
// coerce min api to int
int minApiInt = Integer.parseInt(minApi);
// get module max api and set to 0 if it's "" or null
String maxApi;
if (module.has("maxApi") && !module.getString("maxApi").equals("")) {
maxApi = module.getString("maxApi");
} else {
maxApi = "0";
}
// coerce max api to int
int maxApiInt = Integer.parseInt(maxApi);
// get module min magisk
String minMagisk;
if (module.has("minMagisk") && !module.getString("minMagisk").equals("")) {
minMagisk = module.getString("minMagisk");
} else {
minMagisk = "0";
}
// coerce min magisk to int
int minMagiskInt = Integer.parseInt(minMagisk);
// get module need ramdisk
boolean needRamdisk;
if (module.has("needRamdisk")) {
needRamdisk = module.getBoolean("needRamdisk");
} else {
needRamdisk = false;
}
// get module support
String support;
if (module.has("support")) {
support = module.getString("support");
} else {
support = "";
}
// get module donate
String donate;
if (module.has("donate")) {
donate = module.getString("donate");
} else {
donate = "";
}
// get module config
String config;
if (module.has("config")) {
config = module.getString("config");
} else {
config = "";
}
// get module change boot
boolean changeBoot;
if (module.has("changeBoot")) {
changeBoot = module.getBoolean("changeBoot");
} else {
changeBoot = false;
}
// get module mmt reborn
boolean mmtReborn;
if (module.has("mmtReborn")) {
mmtReborn = module.getBoolean("mmtReborn");
} else {
mmtReborn = false;
}
// try to get updated_at or lastUpdate value for lastUpdate
int lastUpdate;
if (module.has("updated_at")) {
lastUpdate = module.getInt("updated_at");
} else if (module.has("lastUpdate")) {
lastUpdate = module.getInt("lastUpdate");
} else {
lastUpdate = 0;
}
// now downloads or stars
int downloads;
if (module.has("downloads")) {
downloads = module.getInt("downloads");
} else if (module.has("stars")) {
downloads = module.getInt("stars");
} else {
downloads = 0;
}
// get module repo id
String repoId = this.repoData.id;
// get module installed
boolean installed = false;
// get module installed version code
int installedVersionCode = 0;
// get safe property. for now, only supported by androidacy repo and they use "vt_status" key
boolean safe = false;
if (this.repoData.getName().equals("Androidacy Modules Repo")) {
if (module.has("vt_status")) {
if (module.getString("vt_status").equals("Clean")) {
safe = true;
}
}
}
// insert module to realm
// first create a collection of all the properties
// then insert to realm
// then commit
// then close
if (realm.isInTransaction()) {
realm.cancelTransaction();
}
// create a realm object and insert or update it
// add everything to the realm object
if (realm.isInTransaction()) {
realm.commitTransaction();
}
realm.beginTransaction();
ModuleListCache moduleListCache = realm.createObject(ModuleListCache.class, id);
moduleListCache.setName(name);
moduleListCache.setVersion(version);
moduleListCache.setVersionCode(versionCode);
moduleListCache.setAuthor(author);
moduleListCache.setDescription(description);
moduleListCache.setMinApi(minApiInt);
moduleListCache.setMaxApi(maxApiInt);
moduleListCache.setMinMagisk(minMagiskInt);
moduleListCache.setNeedRamdisk(needRamdisk);
moduleListCache.setSupport(support);
moduleListCache.setDonate(donate);
moduleListCache.setConfig(config);
moduleListCache.setChangeBoot(changeBoot);
moduleListCache.setMmtReborn(mmtReborn);
moduleListCache.setRepoId(repoId);
moduleListCache.setInstalled(installed);
moduleListCache.setInstalledVersionCode(installedVersionCode);
moduleListCache.setSafe(safe);
moduleListCache.setLastUpdate(lastUpdate);
moduleListCache.setStats(downloads);
realm.copyToRealmOrUpdate(moduleListCache);
realm.commitTransaction();
} catch (
Exception ignored) {
}
}
realm.close();
} catch (
Exception ignored) {
}
this.indexRaw = null;
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm2 = Realm.getInstance(realmConfiguration2);
if (realm2.isInTransaction()) {
realm2.cancelTransaction();
}
// set lastUpdate
realm2.executeTransaction(r -> {
ReposList repoListCache = r.where(ReposList.class).equalTo("id", this.repoData.id).findFirst();
if (repoListCache != null) {
success.set(true);
// get unix timestamp of current time
int currentTime = (int) (System.currentTimeMillis() / 1000);
Timber.d("Updating lastUpdate for repo %s to %s which is %s seconds ago", this.repoData.id, currentTime, (currentTime - repoListCache.getLastUpdate()));
repoListCache.setLastUpdate(currentTime);
} else {
Timber.w("Failed to update lastUpdate for repo %s", this.repoData.id);
}
});
realm2.close();
} else {
success.set(true); // assume we're reading from cache. this may be unsafe but it's better than nothing
}
this.toUpdate = null;
this.toApply = null;
return success;
return success.get();
}
}

@ -0,0 +1,64 @@
package com.fox2code.mmm.settings;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
public class LongClickablePreference extends Preference {
private OnPreferenceLongClickListener onPreferenceLongClickListener;
public LongClickablePreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public LongClickablePreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public LongClickablePreference(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public LongClickablePreference(@NonNull Context context) {
super(context);
}
@Override
public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
holder.itemView.setOnLongClickListener(v -> performLongClick());
}
private boolean performLongClick() {
if (!this.isEnabled() || !this.isSelectable()) {
return false;
}
if (this.onPreferenceLongClickListener != null) {
return this.onPreferenceLongClickListener.onPreferenceLongClick(this);
}
return false;
}
public void setOnPreferenceLongClickListener(OnPreferenceLongClickListener onPreferenceLongClickListener) {
this.onPreferenceLongClickListener = onPreferenceLongClickListener;
}
public OnPreferenceLongClickListener getOnPreferenceLongClickListener() {
return this.onPreferenceLongClickListener;
}
@FunctionalInterface
public interface OnPreferenceLongClickListener {
/**
* Called when a preference has been clicked.
*
* @param preference The preference that was clicked
* @return {@code true} if the click was handled
*/
boolean onPreferenceLongClick(@NonNull Preference preference);
}
}

@ -0,0 +1,89 @@
package com.fox2code.mmm.settings;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceDataStore;
import java.util.Set;
import timber.log.Timber;
public class SharedPreferenceDataStore extends PreferenceDataStore {
private final SharedPreferences mSharedPreferences;
public SharedPreferenceDataStore(@NonNull SharedPreferences sharedPreferences) {
Timber.d("SharedPreferenceDataStore: %s", sharedPreferences);
mSharedPreferences = sharedPreferences;
}
@NonNull
public SharedPreferences getSharedPreferences() {
Timber.d("getSharedPreferences: %s", mSharedPreferences);
return mSharedPreferences;
}
@Override
public void putString(String key, @Nullable String value) {
mSharedPreferences.edit().putString(key, value).apply();
}
@Override
public void putStringSet(String key, @Nullable Set<String> values) {
mSharedPreferences.edit().putStringSet(key, values).apply();
}
@Override
public void putInt(String key, int value) {
mSharedPreferences.edit().putInt(key, value).apply();
}
@Override
public void putLong(String key, long value) {
mSharedPreferences.edit().putLong(key, value).apply();
}
@Override
public void putFloat(String key, float value) {
mSharedPreferences.edit().putFloat(key, value).apply();
}
@Override
public void putBoolean(String key, boolean value) {
mSharedPreferences.edit().putBoolean(key, value).apply();
}
@Nullable
@Override
public String getString(String key, @Nullable String defValue) {
return mSharedPreferences.getString(key, defValue);
}
@Nullable
@Override
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
return mSharedPreferences.getStringSet(key, defValues);
}
@Override
public int getInt(String key, int defValue) {
return mSharedPreferences.getInt(key, defValue);
}
@Override
public long getLong(String key, long defValue) {
return mSharedPreferences.getLong(key, defValue);
}
@Override
public float getFloat(String key, float defValue) {
return mSharedPreferences.getFloat(key, defValue);
}
@Override
public boolean getBoolean(String key, boolean defValue) {
return mSharedPreferences.getBoolean(key, defValue);
}
}

@ -1,70 +0,0 @@
package com.fox2code.mmm.utils;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Build;
import android.view.ViewGroup;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import eightbitlab.com.blurview.BlurAlgorithm;
import eightbitlab.com.blurview.BlurView;
import eightbitlab.com.blurview.RenderEffectBlur;
import eightbitlab.com.blurview.RenderScriptBlur;
public class BlurUtils {
public static void setupBlur(BlurView blurView, Activity activity, @IdRes int viewId) {
setupBlur(blurView, activity, activity.findViewById(viewId));
}
@SuppressWarnings("deprecation")
public static void setupBlur(BlurView blurView, Activity activity, ViewGroup rootView) {
blurView.setupWith(rootView, new BlurAlgorithmWrapper(
Build.VERSION.SDK_INT < Build.VERSION_CODES.S ?
new RenderScriptBlur(blurView.getContext()) : new RenderEffectBlur()))
.setFrameClearDrawable(activity.getWindow().getDecorView().getBackground())
.setBlurRadius(4F).setBlurAutoUpdate(true);
}
// Allow to have fancy blur, use more performance.
private static final class BlurAlgorithmWrapper implements BlurAlgorithm {
private final BlurAlgorithm algorithm;
private BlurAlgorithmWrapper(BlurAlgorithm algorithm) {
this.algorithm = algorithm;
}
@Override
public Bitmap blur(Bitmap bitmap, float blurRadius) {
return this.algorithm.blur(bitmap, blurRadius * 6f);
}
@Override
public void destroy() {
this.algorithm.destroy();
}
@Override
public boolean canModifyBitmap() {
return this.algorithm.canModifyBitmap();
}
@NonNull
@Override
public Bitmap.Config getSupportedBitmapConfig() {
return this.algorithm.getSupportedBitmapConfig();
}
@Override
public float scaleFactor() {
return 1f;
}
@Override
public void render(@NonNull Canvas canvas, @NonNull Bitmap bitmap) {
this.algorithm.render(canvas, bitmap);
}
}
}

@ -0,0 +1,47 @@
package com.fox2code.mmm.utils;
import android.content.Context;
import android.content.res.Resources;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.LinearLayoutCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
// ProgressDialog is deprecated because it's an bad UX pattern, but sometimes we have no other choice...
public enum BudgetProgressDialog {
;
public static AlertDialog build(Context context, String title, String message) {
Resources r = context.getResources();
int padding = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, r.getDisplayMetrics()));
LinearLayoutCompat v = new LinearLayoutCompat(context);
v.setOrientation(LinearLayoutCompat.HORIZONTAL);
ProgressBar pb = new ProgressBar(context);
v.addView(pb, new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
TextView t = new TextView(context);
t.setGravity(Gravity.CENTER);
v.addView(t, new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, 4));
v.setPadding(padding, padding, padding, padding);
t.setText(message);
return new MaterialAlertDialogBuilder(context)
.setTitle(title)
.setView(v)
.setCancelable(false)
.create();
}
public static AlertDialog build(Context context, int title, String message) {
return build(context, context.getString(title), message);
}
public static AlertDialog build(Context context, int title, int message) {
return build(context, title, context.getString(message));
}
}

@ -9,7 +9,6 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
@ -21,44 +20,41 @@ import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.util.List;
import timber.log.Timber;
public final class ExternalHelper {
private static final String TAG = "ExternalHelper";
public static final ExternalHelper INSTANCE = new ExternalHelper();
private static final boolean TEST_MODE = false;
private static final String FOX_MMM_OPEN_EXTERNAL =
"com.fox2code.mmm.utils.intent.action.OPEN_EXTERNAL";
private static final String FOX_MMM_OPEN_EXTERNAL = "com.fox2code.mmm.utils.intent.action.OPEN_EXTERNAL";
private static final String FOX_MMM_EXTRA_REPO_ID = "extra_repo_id";
public static final ExternalHelper INSTANCE = new ExternalHelper();
private ComponentName fallback;
private CharSequence label;
private boolean multi;
private ExternalHelper() {}
private ExternalHelper() {
}
public void refreshHelper(Context context) {
Intent intent = new Intent(FOX_MMM_OPEN_EXTERNAL,
Uri.parse("https://fox2code.com/module.zip"));
List<ResolveInfo> resolveInfos = context.getPackageManager()
.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);
if (resolveInfos == null || resolveInfos.isEmpty()) {
Log.d(TAG, "No external provider installed!");
Intent intent = new Intent(FOX_MMM_OPEN_EXTERNAL, Uri.parse("https://fox2code.com/module.zip"));
List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);
if (resolveInfos.isEmpty()) {
Timber.i("No external provider installed!");
label = TEST_MODE ? "External" : null;
multi = TEST_MODE;
fallback = null;
} else {
ResolveInfo resolveInfo = resolveInfos.get(0);
Log.d(TAG, "Found external provider: " + resolveInfo.activityInfo.packageName);
fallback = new ComponentName(
resolveInfo.activityInfo.packageName,
resolveInfo.activityInfo.name);
Timber.i("Found external provider: %s", resolveInfo.activityInfo.packageName);
fallback = new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
label = resolveInfo.loadLabel(context.getPackageManager());
multi = resolveInfos.size() >= 2;
}
}
public boolean openExternal(Context context, Uri uri, String repoId) {
if (label == null) return false;
Bundle param = ActivityOptionsCompat.makeCustomAnimation(context,
android.R.anim.fade_in, android.R.anim.fade_out).toBundle();
if (label == null)
return false;
Bundle param = ActivityOptionsCompat.makeCustomAnimation(context, android.R.anim.fade_in, android.R.anim.fade_out).toBundle();
Intent intent = new Intent(FOX_MMM_OPEN_EXTERNAL, uri);
intent.setFlags(IntentHelper.FLAG_GRANT_URI_PERMISSION);
intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId);
@ -74,8 +70,9 @@ public final class ExternalHelper {
context.startActivity(intent, param);
}
return true;
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Failed to launch activity", e);
} catch (
ActivityNotFoundException e) {
Timber.e(e);
}
if (fallback != null) {
if (multi) {
@ -87,27 +84,28 @@ public final class ExternalHelper {
try {
context.startActivity(intent, param);
return true;
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Failed to launch fallback", e);
} catch (
ActivityNotFoundException e) {
Timber.e(e);
}
}
return false;
}
public void injectButton(AlertDialog.Builder builder, Supplier<Uri> uriSupplier, String repoId) {
if (label == null) return;
if (label == null)
return;
builder.setNeutralButton(label, (dialog, button) -> {
Context context = ((Dialog) dialog).getContext();
new Thread("Async downloader") {
@Override
public void run() {
final Uri uri = uriSupplier.get();
if (uri == null) return;
if (uri == null)
return;
UiThreadHandler.run(() -> {
if (!openExternal(context, uri, repoId)) {
Toast.makeText(context,
"Failed to launch external activity",
Toast.LENGTH_SHORT).show();
Toast.makeText(context, "Failed to launch external activity", Toast.LENGTH_SHORT).show();
}
});
}

@ -1,133 +0,0 @@
package com.fox2code.mmm.utils;
import android.os.Build;
import androidx.annotation.NonNull;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import com.topjohnwu.superuser.io.SuFileOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
public class Files {
private static final boolean is64bit = Build.SUPPORTED_64_BIT_ABIS.length > 0;
public static void write(File file, byte[] bytes) throws IOException {
try (OutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(bytes);
outputStream.flush();
}
}
public static byte[] read(File file) throws IOException {
try (InputStream inputStream = new FileInputStream(file)) {
return readAllBytes(inputStream);
}
}
public static void writeSU(File file, byte[] bytes) throws IOException {
try (OutputStream outputStream = SuFileOutputStream.open(file)) {
outputStream.write(bytes);
outputStream.flush();
}
}
public static byte[] readSU(File file) throws IOException {
if (file.isFile() && file.canRead()) {
try { // Read as app if su not required
return read(file);
} catch (IOException ignored) {}
}
try (InputStream inputStream = SuFileInputStream.open(file)) {
return readAllBytes(inputStream);
}
}
public static boolean existsSU(File file) {
return file.exists() || new SuFile(file.getAbsolutePath()).exists();
}
public static void copy(InputStream inputStream,OutputStream outputStream) throws IOException {
int nRead;
byte[] data = new byte[16384];
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
outputStream.write(data, 0, nRead);
}
outputStream.flush();
}
public static void closeSilently(Closeable closeable) {
try {
if (closeable != null) closeable.close();
} catch (IOException ignored) {}
}
public static ByteArrayOutputStream makeBuffer(long capacity) {
// Cap buffer to 1 Gib (or 512 Mib for 32bit) to avoid memory errors
return Files.makeBuffer((int) Math.min(capacity, is64bit ? 0x40000000 : 0x20000000));
}
public static ByteArrayOutputStream makeBuffer(int capacity) {
return new ByteArrayOutputStream(Math.max(0x20, capacity)) {
@NonNull
@Override
public byte[] toByteArray() {
return this.buf.length == this.count ?
this.buf : super.toByteArray();
}
};
}
public static byte[] readAllBytes(InputStream inputStream) throws IOException {
ByteArrayOutputStream buffer = Files.makeBuffer(inputStream.available());
copy(inputStream, buffer);
return buffer.toByteArray();
}
public static void fixJavaZipHax(byte[] bytes) {
if (bytes.length > 8 && bytes[0x6] == 0x0 && bytes[0x7] == 0x0 && bytes[0x8] == 0x8)
bytes[0x7] = 0x8; // Known hax to prevent java zip file read
}
public static void patchModuleSimple(byte[] bytes,OutputStream outputStream) throws IOException {
fixJavaZipHax(bytes); patchModuleSimple(new ByteArrayInputStream(bytes), outputStream);
}
public static void patchModuleSimple(InputStream inputStream,OutputStream outputStream) throws IOException {
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream);
int nRead;
byte[] data = new byte[16384];
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
String name = zipEntry.getName();
int i = name.indexOf('/', 1);
if (i == -1) continue;
String newName = name.substring(i + 1);
if (newName.startsWith(".git")) continue; // Skip metadata
zipOutputStream.putNextEntry(new ZipEntry(newName));
while ((nRead = zipInputStream.read(data, 0, data.length)) != -1) {
zipOutputStream.write(data, 0, nRead);
}
zipOutputStream.flush();
zipOutputStream.closeEntry();
zipInputStream.closeEntry();
}
zipOutputStream.finish();
zipOutputStream.flush();
zipOutputStream.close();
zipInputStream.close();
}
}

@ -1,165 +0,0 @@
package com.fox2code.mmm.utils;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import java.util.regex.Pattern;
public class Hashes {
private static final String TAG = "Hashes";
private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
private static final Pattern nonAlphaNum = Pattern.compile("[^a-zA-Z0-9]");
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
public static String hashMd5(byte[] input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
return bytesToHex(md.digest(input));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String hashSha1(byte[] input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
return bytesToHex(md.digest(input));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String hashSha256(byte[] input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return bytesToHex(md.digest(input));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String hashSha512(byte[] input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
return bytesToHex(md.digest(input));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* Check if the checksum match a file by picking the correct
* hashing algorithm depending on the length of the checksum
*/
public static boolean checkSumMatch(byte[] data, String checksum) {
String hash;
if (checksum == null) return false;
switch (checksum.length()) {
case 0:
return true; // No checksum
case 32:
hash = Hashes.hashMd5(data); break;
case 40:
hash = Hashes.hashSha1(data); break;
case 64:
hash = Hashes.hashSha256(data); break;
case 128:
hash = Hashes.hashSha512(data); break;
default:
Log.e(TAG, "No hash algorithm for " +
checksum.length() * 8 + "bit checksums");
return false;
}
Log.d(TAG, "Checksum result (data: " + hash+ ",expected: " + checksum + ")");
return hash.equals(checksum.toLowerCase(Locale.ROOT));
}
/**
* Check if the checksum match a file by picking the correct
* hashing algorithm depending on the length of the checksum
*/
public static boolean checkSumMatch(InputStream data, String checksum) throws IOException {
String hash;
if (checksum == null) return false;
String checksumAlgorithm = checkSumName(checksum);
if (checksumAlgorithm == null) {
Log.e(TAG, "No hash algorithm for " +
checksum.length() * 8 + "bit checksums");
return false;
}
try {
MessageDigest md = MessageDigest.getInstance(checksumAlgorithm);
byte[] bytes = new byte[2048];
int nRead;
while ((nRead = data.read(bytes)) > 0) {
md.update(bytes, 0, nRead);
}
hash = bytesToHex(md.digest());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
Log.d(TAG, "Checksum result (data: " + hash + ",expected: " + checksum + ")");
return hash.equals(checksum.toLowerCase(Locale.ROOT));
}
public static boolean checkSumValid(String checksum) {
if (checksum == null) return false;
switch (checksum.length()) {
case 0:
default:
return false;
case 32:
case 40:
case 64:
case 128:
final int len = checksum.length();
for (int i = 0; i < len; i++) {
char c = checksum.charAt(i);
if (c < '0' || c > 'f') return false;
if (c > '9' && // Easier working with bits
(c & 0b01011111) < 'A') return false;
}
return true;
}
}
public static String checkSumName(String checksum) {
if (checksum == null) return null;
switch (checksum.length()) {
case 0:
default:
return null;
case 32:
return "MD5";
case 40:
return "SHA-1";
case 64:
return "SHA-256";
case 128:
return "SHA-512";
}
}
public static String checkSumFormat(String checksum) {
if (checksum == null) return null;
// Remove all non-alphanumeric characters
return nonAlphaNum.matcher(checksum.trim()).replaceAll("");
}
}

@ -1,600 +0,0 @@
package com.fox2code.mmm.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.WebSettings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.androidacy.AndroidacyUtil;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.repo.RepoManager;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Proxy;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import io.sentry.android.okhttp.SentryOkHttpInterceptor;
import okhttp3.Cache;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.Dns;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.brotli.BrotliInterceptor;
import okhttp3.dnsoverhttps.DnsOverHttps;
import okio.BufferedSink;
public class Http {
private static final String TAG = "Http";
private static final OkHttpClient httpClient;
private static final OkHttpClient httpClientDoH;
private static final OkHttpClient httpClientWithCache;
private static final OkHttpClient httpClientWithCacheDoH;
private static final OkHttpClient httpClientNoRedirect;
private static final OkHttpClient httpClientNoRedirectDoH;
private static final FallBackDNS fallbackDNS;
private static final CDNCookieJar cookieJar;
private static final String androidacyUA;
private static final boolean hasWebView;
private static boolean doh;
static {
MainApplication mainApplication = MainApplication.getINSTANCE();
if (mainApplication == null) {
Error error = new Error("Initialized Http too soon!");
error.fillInStackTrace();
Log.e(TAG, "Initialized Http too soon!", error);
System.out.flush();
System.err.flush();
try {
Os.kill(Os.getpid(), 9);
} catch (ErrnoException e) {
System.exit(9);
}
throw error;
}
CookieManager cookieManager;
try {
cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.flush(); // Make sure the instance work
} catch (Throwable t) {
cookieManager = null;
Log.e(TAG, "No WebView support!", t);
}
hasWebView = cookieManager != null;
OkHttpClient.Builder httpclientBuilder = new OkHttpClient.Builder();
// Default is 10, extend it a bit for slow mobile connections.
httpclientBuilder.connectTimeout(15, TimeUnit.SECONDS);
httpclientBuilder.writeTimeout(15, TimeUnit.SECONDS);
httpclientBuilder.readTimeout(15, TimeUnit.SECONDS);
httpclientBuilder.addInterceptor(BrotliInterceptor.INSTANCE);
httpclientBuilder.proxy(Proxy.NO_PROXY); // Do not use system proxy
Dns dns = Dns.SYSTEM;
try {
InetAddress[] cloudflareBootstrap = new InetAddress[] {
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")
};
dns = s -> {
if ("cloudflare-dns.com".equals(s)) {
return Arrays.asList(cloudflareBootstrap);
}
return Dns.SYSTEM.lookup(s);
};
httpclientBuilder.dns(dns);
httpclientBuilder.cookieJar(new CDNCookieJar());
dns = new DnsOverHttps.Builder().client(httpclientBuilder.build()).url(
Objects.requireNonNull(HttpUrl.parse("https://cloudflare-dns.com/dns-query")))
.bootstrapDnsHosts(cloudflareBootstrap).resolvePrivateAddresses(true).build();
} catch (UnknownHostException|RuntimeException e) {
Log.e(TAG, "Failed to init DoH", e);
}
httpclientBuilder.cookieJar(CookieJar.NO_COOKIES);
// User-Agent format was agreed on telegram
if (hasWebView) {
androidacyUA = WebSettings.getDefaultUserAgent(mainApplication).replace("; wv", "")
+ " FoxMmm/" + BuildConfig.VERSION_CODE;
} else {
androidacyUA = "Mozilla/5.0 (Linux; Android " + Build.VERSION.RELEASE + "; " + Build.DEVICE +")" +
" AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Mobile Safari/537.36"
+ " FoxMmm/" + BuildConfig.VERSION_CODE;
}
httpclientBuilder.addInterceptor(chain -> {
Request.Builder request = chain.request().newBuilder();
request.header("Upgrade-Insecure-Requests", "1");
String host = chain.request().url().host();
if (host.endsWith(".androidacy.com")) {
request.header("User-Agent", androidacyUA);
} else if (!(host.equals("github.com") || host.endsWith(".github.com") ||
host.endsWith(".jsdelivr.net") || host.endsWith(".githubusercontent.com"))) {
if (InstallerInitializer.peekMagiskPath() != null) {
request.header("User-Agent", // Declare Magisk version to the server
"Magisk/" + InstallerInitializer.peekMagiskVersion());
}
}
if (chain.request().header("Accept-Language") == null) {
request.header("Accept-Language", // Send system language to the server
mainApplication.getResources().getConfiguration().locale.toLanguageTag());
}
return chain.proceed(request.build());
});
// Fallback DNS cache responses in case request fail but already succeeded once in the past
fallbackDNS = new FallBackDNS(mainApplication, dns,
"github.com", "api.github.com", "raw.githubusercontent.com",
"camo.githubusercontent.com", "user-images.githubusercontent.com",
"cdn.jsdelivr.net", "img.shields.io", "magisk-modules-repo.github.io",
"www.androidacy.com", "api.androidacy.com");
httpclientBuilder.cookieJar(cookieJar = new CDNCookieJar(cookieManager));
httpclientBuilder.dns(Dns.SYSTEM);
httpClient = followRedirects(httpclientBuilder, true).build();
httpClientNoRedirect = followRedirects(httpclientBuilder, false).build();
httpclientBuilder.dns(fallbackDNS);
httpClientDoH = followRedirects(httpclientBuilder, true).build();
httpClientNoRedirectDoH = followRedirects(httpclientBuilder, false).build();
httpclientBuilder.cache(new Cache(
new File(mainApplication.getCacheDir(), "http_cache"),
16L * 1024L * 1024L)); // 16Mib of cache
httpclientBuilder.dns(Dns.SYSTEM);
httpClientWithCache = followRedirects(httpclientBuilder, true).build();
httpclientBuilder.dns(fallbackDNS);
httpClientWithCacheDoH = followRedirects(httpclientBuilder, true).build();
Log.i(TAG, "Initialized Http successfully!");
doh = MainApplication.isDohEnabled();
}
private static OkHttpClient.Builder followRedirects(
OkHttpClient.Builder builder, boolean followRedirects) {
return builder.followRedirects(followRedirects)
.followSslRedirects(followRedirects);
}
public static OkHttpClient getHttpClient() {
return doh ? httpClientDoH : httpClient;
}
public static OkHttpClient getHttpClientNoRedirect() {
return doh ? httpClientNoRedirectDoH : httpClientNoRedirect;
}
public static OkHttpClient getHttpClientWithCache() {
return doh ? httpClientWithCacheDoH : httpClientWithCache;
}
@SuppressWarnings("resource")
public static byte[] doHttpGet(String url,boolean allowCache) throws IOException {
if (!RepoManager.isAndroidacyRepoEnabled() &&
AndroidacyUtil.isAndroidacyLink(url)) {
throw new IOException("Androidacy repo is disabled, blocking url: " + url);
}
Response response = (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(
new Request.Builder().url(url).get().build()
).execute();
// 200/204 == success, 304 == cache valid
if (response.code() != 200 && response.code() != 204 &&
(response.code() != 304 || !allowCache)) {
throw new IOException("Received error code: "+ response.code());
}
ResponseBody responseBody = response.body();
// Use cache api if used cached response
if (responseBody == null && response.code() == 304) {
response = response.cacheResponse();
if (response != null)
responseBody = response.body();
}
return responseBody == null ? new byte[0] : responseBody.bytes();
}
public static byte[] doHttpPost(String url,String data,boolean allowCache) throws IOException {
return (byte[]) doHttpPostRaw(url, data, allowCache, false);
}
public static String doHttpPostRedirect(String url, String data, boolean allowCache) throws IOException {
return (String) doHttpPostRaw(url, data, allowCache, true);
}
@SuppressWarnings("resource")
private static Object doHttpPostRaw(String url,String data, boolean allowCache,
boolean isRedirect) throws IOException {
if (!RepoManager.isAndroidacyRepoEnabled() &&
AndroidacyUtil.isAndroidacyLink(url)) {
throw new IOException("Androidacy repo is disabled, blocking url: " + url);
}
Response response = (isRedirect ? getHttpClientNoRedirect() :
allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(
new Request.Builder().url(url).post(JsonRequestBody.from(data))
.header("Content-Type", "application/json").build()
).execute();
if (isRedirect && response.isRedirect()) {
return response.request().url().uri().toString();
}
// 200/204 == success, 304 == cache valid
if (response.code() != 200 && response.code() != 204 &&
(response.code() != 304 || !allowCache)) {
throw new IOException("Received error code: "+ response.code());
}
if (isRedirect) return url;
ResponseBody responseBody = response.body();
// Use cache api if used cached response
if (responseBody == null && response.code() == 304) {
response = response.cacheResponse();
if (response != null)
responseBody = response.body();
}
return responseBody == null ? new byte[0] : responseBody.bytes();
}
public static byte[] doHttpGet(String url,ProgressListener progressListener) throws IOException {
Log.d("Http", "Progress URL: " + url);
if (!RepoManager.isAndroidacyRepoEnabled() &&
AndroidacyUtil.isAndroidacyLink(url)) {
throw new IOException("Androidacy repo is disabled, blocking url: " + url);
}
Response response = getHttpClient().newCall(
new Request.Builder().url(url).get().build()).execute();
if (response.code() != 200 && response.code() != 204) {
throw new IOException("Received error code: "+ response.code());
}
ResponseBody responseBody = Objects.requireNonNull(response.body());
InputStream inputStream = responseBody.byteStream();
byte[] buff = new byte[1024 * 4];
long downloaded = 0;
long target = responseBody.contentLength();
ByteArrayOutputStream byteArrayOutputStream = Files.makeBuffer(target);
int divider = 1; // Make everything go in an int
while ((target / divider) > (Integer.MAX_VALUE / 2)) {
divider *= 2;
}
final long UPDATE_INTERVAL = 100;
long nextUpdate = System.currentTimeMillis() + UPDATE_INTERVAL;
long currentUpdate;
Log.d("Http", "Target: " + target + " Divider: " + divider);
progressListener.onUpdate(0, (int) (target / divider), false);
while (true) {
int read = inputStream.read(buff);
if(read == -1) break;
byteArrayOutputStream.write(buff, 0, read);
downloaded += read;
currentUpdate = System.currentTimeMillis();
if (nextUpdate < currentUpdate) {
nextUpdate = currentUpdate + UPDATE_INTERVAL;
progressListener.onUpdate((int) (downloaded / divider), (int) (target / divider), false);
}
}
inputStream.close();
progressListener.onUpdate((int) (downloaded / divider), (int) (target / divider), true);
return byteArrayOutputStream.toByteArray();
}
public static void cleanDnsCache() {
if (Http.fallbackDNS != null) {
Http.fallbackDNS.cleanDnsCache();
}
}
public static String getAndroidacyUA() {
return androidacyUA;
}
public static String getMagiskUA() {
return "Magisk/" + InstallerInitializer.peekMagiskVersion();
}
public static void setDoh(boolean doh) {
Log.d(TAG, "DoH: " + Http.doh + " -> " + doh);
Http.doh = doh;
}
public static String getAndroidacyCookies(String url) {
if (!AndroidacyUtil.isAndroidacyLink(url)) return "";
return cookieJar.getAndroidacyCookies(url);
}
/**
* Cookie jar that allow CDN cookies, reset on app relaunch
* Note: An argument can be made that it allow tracking but
* caching is a better attack vector for tracking, this system
* only exist to improve CDN response time, any other cookies
* that are not CDN related are just simply ignored.
*
* Note: CDNCookies are only stored in RAM unlike https cache
* */
private static class CDNCookieJar implements CookieJar {
private final HashMap<String, Cookie> cookieMap = new HashMap<>();
private final boolean androidacySupport;
private final CookieManager cookieManager;
private List<Cookie> androidacyCookies;
private CDNCookieJar() {
this.androidacySupport = false;
this.cookieManager = null;
}
private CDNCookieJar(CookieManager cookieManager) {
this.androidacySupport = true;
this.cookieManager = cookieManager;
if (cookieManager == null) {
this.androidacyCookies = Collections.emptyList();
}
}
@NonNull
@Override
public List<Cookie> loadForRequest(@NonNull HttpUrl httpUrl) {
if (!httpUrl.isHttps()) return Collections.emptyList();
if (this.androidacySupport && httpUrl.host().endsWith(".androidacy.com")) {
if (this.cookieManager == null) return this.androidacyCookies;
String cookies = this.cookieManager.getCookie(httpUrl.uri().toString());
if (cookies == null || cookies.isEmpty()) return Collections.emptyList();
String[] splitCookies = cookies.split(";");
ArrayList<Cookie> cookieList = new ArrayList<>(splitCookies.length);
for (String cookie : splitCookies) {
cookieList.add(Cookie.parse(httpUrl, cookie));
}
return cookieList;
}
Cookie cookies = cookieMap.get(httpUrl.url().getHost());
return cookies == null || cookies.expiresAt() < System.currentTimeMillis() ?
Collections.emptyList() : Collections.singletonList(cookies);
}
@Override
public void saveFromResponse(@NonNull HttpUrl httpUrl, @NonNull List<Cookie> cookies) {
if (!httpUrl.isHttps()) return;
if (this.androidacySupport && httpUrl.host().endsWith(".androidacy.com")) {
if (this.cookieManager == null) {
if (httpUrl.host().equals(".androidacy.com") || !cookies.isEmpty())
this.androidacyCookies = cookies;
return;
}
for (Cookie cookie : cookies) {
this.cookieManager.setCookie(
httpUrl.uri().toString(), cookie.toString());
}
return;
}
String host = httpUrl.url().getHost();
Iterator<Cookie> cookieIterator = cookies.iterator();
Cookie cdnCookie = cookieMap.get(host);
while (cookieIterator.hasNext()) {
Cookie cookie = cookieIterator.next();
if (host.equals(cookie.domain()) && cookie.secure() && cookie.httpOnly() &&
cookie.expiresAt() < (System.currentTimeMillis() + 1000 * 60 * 60 * 48)) {
if (cdnCookie != null &&
!cdnCookie.name().equals(cookie.name())) {
cookieMap.remove(host);
cdnCookie = null;
break;
} else {
cdnCookie = cookie;
}
}
}
if (cdnCookie == null) {
cookieMap.remove(host);
} else {
cookieMap.put(host, cdnCookie);
}
}
String getAndroidacyCookies(String url) {
if (this.cookieManager != null) {
return this.cookieManager.getCookie(url);
}
StringBuilder stringBuilder = new StringBuilder();
for (Cookie cookie : this.androidacyCookies) {
stringBuilder.append(cookie.toString()).append("; ");
}
stringBuilder.setLength(stringBuilder.length() - 2);
return stringBuilder.toString();
}
}
public interface ProgressListener {
void onUpdate(int downloaded,int total, boolean done);
}
/**
* FallBackDNS store successful DNS request to return them later
* can help make the app to work later when the current DNS system
* isn't functional or available.
*
* Note: DNS Cache is stored in user data.
* */
private static class FallBackDNS implements Dns {
private final Dns parent;
private final SharedPreferences sharedPreferences;
private final HashSet<String> fallbacks;
private final HashMap<String, List<InetAddress>> fallbackCache;
public FallBackDNS(Context context, Dns parent, String... fallbacks) {
this.sharedPreferences = context.getSharedPreferences(
"mmm_dns", Context.MODE_PRIVATE);
this.parent = parent;
this.fallbacks = new HashSet<>(Arrays.asList(fallbacks));
this.fallbackCache = new HashMap<>();
}
@NonNull
@Override
public List<InetAddress> lookup(@NonNull String s) throws UnknownHostException {
if (this.fallbacks.contains(s)) {
List<InetAddress> addresses;
synchronized (this.fallbackCache) {
addresses = this.fallbackCache.get(s);
if (addresses != null)
return addresses;
try {
addresses = this.parent.lookup(s);
if (addresses.isEmpty() || addresses.get(0).isLoopbackAddress())
throw new UnknownHostException(s);
this.fallbackCache.put(s, addresses);
this.sharedPreferences.edit().putString(
s.replace('.', '_'), toString(addresses)).apply();
} catch (UnknownHostException e) {
String key = this.sharedPreferences.getString(
s.replace('.', '_'), "");
if (key.isEmpty()) throw e;
try {
addresses = fromString(key);
this.fallbackCache.put(s, addresses);
} catch (UnknownHostException e2) {
this.sharedPreferences.edit().remove(
s.replace('.', '_')).apply();
throw e;
}
}
}
return addresses;
} else {
return this.parent.lookup(s);
}
}
void cleanDnsCache() {
synchronized (this.fallbackCache) {
this.fallbackCache.clear();
}
}
@NonNull
private static String toString(@NonNull List<InetAddress> inetAddresses) {
if (inetAddresses.isEmpty()) return "";
Iterator<InetAddress> inetAddressIterator = inetAddresses.iterator();
StringBuilder stringBuilder = new StringBuilder();
while (true) {
stringBuilder.append(inetAddressIterator.next().getHostAddress());
if (!inetAddressIterator.hasNext())
return stringBuilder.toString();
stringBuilder.append("|");
}
}
@NonNull
private static List<InetAddress> fromString(@NonNull String string)
throws UnknownHostException {
if (string.isEmpty()) return Collections.emptyList();
String[] strings = string.split("\\|");
ArrayList<InetAddress> inetAddresses = new ArrayList<>(strings.length);
for (String address : strings) {
inetAddresses.add(InetAddress.getByName(address));
}
return inetAddresses;
}
}
private static class JsonRequestBody extends RequestBody {
private static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json");
private static final JsonRequestBody EMPTY = new JsonRequestBody(new byte[0]);
static JsonRequestBody from(String data) {
if (data == null || data.length() == 0) {
return EMPTY;
}
return new JsonRequestBody(data.getBytes(StandardCharsets.UTF_8));
}
final byte[] data;
private JsonRequestBody(byte[] data) {
this.data = data;
}
@Nullable
@Override
public MediaType contentType() {
return JSON_MEDIA_TYPE;
}
@Override
public long contentLength() {
return this.data.length;
}
@Override
public void writeTo(@NonNull BufferedSink bufferedSink) throws IOException {
bufferedSink.write(this.data);
}
}
public static boolean hasWebView() {
return hasWebView;
}
/**
* Change URL to appropriate url and force Magisk link to use latest version.
*/
public static String updateLink(String string) {
if (string.startsWith("https://cdn.jsdelivr.net/gh/Magisk-Modules-Repo/")) {
String tmp = string.substring(48);
int start = tmp.lastIndexOf('@'),
end = tmp.lastIndexOf('/');
if ((end - 8) <= start) return string; // Skip if not a commit id
return "https://raw.githubusercontent.com/" +
tmp.substring(0, start) + "/master" + string.substring(end);
}
if (string.startsWith("https://github.com/Magisk-Modules-Repo/")) {
int i = string.lastIndexOf("/archive/");
if (i != -1 && string.indexOf('/', i + 9) == -1)
return string.substring(0, i + 9) + "master.zip";
}
return string;
}
/**
* Change GitHub user-content url to jsdelivr url
* (Unused but kept as a documentation)
*/
public static String cdnIfyLink(String string) {
if (string.startsWith("https://raw.githubusercontent.com/")) {
String[] tokens = string.substring(34).split("/", 4);
if (tokens.length != 4) return string;
return "https://cdn.jsdelivr.net/gh/" +
tokens[0] + "/" + tokens[1] + "@" + tokens[2] + "/" + tokens[3];
}
if (string.startsWith("https://github.com/")) {
int i = string.lastIndexOf("/archive/");
if (i == -1 || string.indexOf('/', i + 9) != -1)
return string; // Not an archive link
String[] tokens = string.substring(19).split("/", 4);
return "https://cdn.jsdelivr.net/gh/" +
tokens[0] + "/" + tokens[1] + "@" + tokens[2] + "/" + tokens[3];
}
return string;
}
}

@ -3,23 +3,20 @@ package com.fox2code.mmm.utils;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.util.TypedValue;
import android.widget.Toast;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.app.BundleCompat;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
@ -28,6 +25,8 @@ import com.fox2code.mmm.XHooks;
import com.fox2code.mmm.androidacy.AndroidacyActivity;
import com.fox2code.mmm.installer.InstallerActivity;
import com.fox2code.mmm.markdown.MarkdownActivity;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.net.Http;
import com.topjohnwu.superuser.CallbackList;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFileInputStream;
@ -38,8 +37,10 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
public class IntentHelper {
private static final String TAG = "IntentHelper";
import timber.log.Timber;
public enum IntentHelper {
;
private static final String EXTRA_TAB_SESSION =
"android.support.customtabs.extra.SESSION";
private static final String EXTRA_TAB_COLOR_SCHEME =
@ -50,18 +51,14 @@ public class IntentHelper {
"android.support.customtabs.extra.TOOLBAR_COLOR";
private static final String EXTRA_TAB_EXIT_ANIMATION_BUNDLE =
"android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
static final int FLAG_GRANT_URI_PERMISSION =
Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP ?
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION :
Intent.FLAG_GRANT_READ_URI_PERMISSION;
static final int FLAG_GRANT_URI_PERMISSION = Intent.FLAG_GRANT_READ_URI_PERMISSION;
public static void openUri(Context context, String uri) {
if (uri.startsWith("intent://")) {
try {
startActivity(context, Intent.parseUri(uri, Intent.URI_INTENT_SCHEME), false);
} catch (URISyntaxException | ActivityNotFoundException e) {
Log.e(TAG, "Failed launch of " + uri, e);
Timber.e(e);
}
} else openUrl(context, uri);
}
@ -71,6 +68,7 @@ public class IntentHelper {
}
public static void openUrl(Context context, String url, boolean forceBrowser) {
Timber.d("Opening url: %s, forced browser %b", url, forceBrowser);
try {
Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
myIntent.setFlags(FLAG_GRANT_URI_PERMISSION);
@ -79,13 +77,14 @@ public class IntentHelper {
}
startActivity(context, myIntent, false);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, "No application can handle this request.\n"
+ " Please install a web-browser", Toast.LENGTH_SHORT).show();
e.printStackTrace();
Timber.d(e, "Could not find suitable activity to handle url");
Toast.makeText(context, FoxActivity.getFoxActivity(context).getString(
R.string.no_browser), Toast.LENGTH_LONG).show();
}
}
public static void openCustomTab(Context context, String url) {
Timber.d("Opening url: %s in custom tab", url);
try {
Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
viewIntent.setFlags(FLAG_GRANT_URI_PERMISSION);
@ -94,9 +93,9 @@ public class IntentHelper {
tabIntent.addCategory(Intent.CATEGORY_BROWSABLE);
startActivityEx(context, tabIntent, viewIntent);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, "No application can handle this request.\n"
+ " Please install a web-browser", Toast.LENGTH_SHORT).show();
e.printStackTrace();
Timber.d(e, "Could not find suitable activity to handle url");
Toast.makeText(context, FoxActivity.getFoxActivity(context).getString(
R.string.no_browser), Toast.LENGTH_LONG).show();
}
}
@ -107,7 +106,7 @@ public class IntentHelper {
public static void openUrlAndroidacy(Context context, String url, boolean allowInstall,
String title,String config) {
if (!Http.hasWebView()) {
Log.w(TAG, "Using custom tab for: " + url);
Timber.w("Using custom tab for: %s", url);
openCustomTab(context, url);
return;
}
@ -126,7 +125,6 @@ public class IntentHelper {
} catch (ActivityNotFoundException e) {
Toast.makeText(context, "No application can handle this request."
+ " Please install a web-browser", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
@ -152,10 +150,10 @@ public class IntentHelper {
"am start -a android.intent.action.MAIN " +
"-c org.lsposed.manager.LAUNCH_MANAGER " +
"com.android.shell/.BugreportWarningActivity")
.to(new CallbackList<String>() {
.to(new CallbackList<>() {
@Override
public void onAddElement(String str) {
Log.d(TAG, "LSPosed: " + str);
Timber.i("LSPosed: %s", str);
}
}).submit();
return;
@ -168,7 +166,6 @@ public class IntentHelper {
} catch (ActivityNotFoundException e) {
Toast.makeText(context,
"Failed to launch module config activity", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
@ -189,15 +186,9 @@ public class IntentHelper {
} catch (ActivityNotFoundException e) {
Toast.makeText(context,
"Failed to launch markdown activity", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
public static void openInstaller(Context context, String url, String title,
String config, String checksum) {
openInstaller(context, url, title, config, checksum, false);
}
public static void openInstaller(Context context, String url, String title, String config,
String checksum, boolean mmtReborn) {
openInstaller(context, url, title, config, checksum, mmtReborn, false);
@ -223,18 +214,9 @@ public class IntentHelper {
} catch (ActivityNotFoundException e) {
Toast.makeText(context,
"Failed to launch markdown activity", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
public static void startActivity(Context context, Intent intent) {
ComponentName componentName = intent.getComponent();
String packageName = context.getPackageName();
startActivity(context, intent, packageName.equals(intent.getPackage()) ||
(componentName != null &&
packageName.equals(componentName.getPackageName())));
}
public static void startActivity(Context context, Class<? extends Activity> activityClass) {
startActivity(context, new Intent(context, activityClass), true);
}
@ -338,7 +320,7 @@ public class IntentHelper {
OnFileReceivedCallback callback) {
File destinationFolder;
if (destination == null || (destinationFolder = destination.getParentFile()) == null ||
(!destinationFolder.isDirectory() && !destinationFolder.mkdirs())) {
(!destinationFolder.mkdirs() && !destinationFolder.isDirectory())) {
callback.onReceived(destination, null, RESPONSE_ERROR);
return;
}
@ -360,7 +342,7 @@ public class IntentHelper {
callback.onReceived(destination, null, RESPONSE_ERROR);
return;
}
Log.d(TAG, "FilePicker returned " + uri);
Timber.i("FilePicker returned %s", uri);
if ("http".equals(uri.getScheme()) ||
"https".equals(uri.getScheme())) {
callback.onReceived(destination, uri, RESPONSE_URL);
@ -390,10 +372,10 @@ public class IntentHelper {
}
outputStream = new FileOutputStream(destination);
Files.copy(inputStream, outputStream);
Log.d(TAG, "File saved at " + destination);
Timber.i("File saved at %s", destination);
success = true;
} catch (Exception e) {
Log.e(TAG, "failed copy of " + uri, e);
Timber.e(e);
Toast.makeText(compatActivity,
R.string.file_picker_failure,
Toast.LENGTH_SHORT).show();
@ -401,7 +383,7 @@ public class IntentHelper {
Files.closeSilently(inputStream);
Files.closeSilently(outputStream);
if (!success && destination.exists() && !destination.delete())
Log.e(TAG, "Failed to delete artefact!");
Timber.e("Failed to delete artefact!");
}
callback.onReceived(destination, uri, success ? RESPONSE_FILE : RESPONSE_ERROR);
});

@ -1,171 +0,0 @@
package com.fox2code.mmm.utils;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.util.Log;
import android.widget.TextView;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import java.lang.ref.WeakReference;
import java.util.LinkedList;
import java.util.Objects;
public class NoodleDebug {
private static final String TAG = "NoodleDebug";
private static final WeakReference<Thread> NULL_THREAD_REF = new WeakReference<>(null);
private static final ThreadLocal<NoodleDebug> THREAD_NOODLE = new ThreadLocal<>();
@SuppressLint("StaticFieldLeak") // <- Null initialized
private static final NoodleDebug NULL = new NoodleDebug() {
@Override
public NoodleDebug bind() {
getNoodleDebug().unbind();
THREAD_NOODLE.remove();
return this;
}
@Override
public void setEnabled(boolean enabled) {}
@Override
protected void markDirty() {}
};
private final Activity activity;
private final TextView textView;
private final LinkedList<String> tokens;
private final StringBuilder debug;
private WeakReference<Thread> thread;
private boolean enabled, updating;
private NoodleDebug() {
this.activity = null;
this.textView = null;
this.tokens = new LinkedList<>();
this.debug = new StringBuilder(0);
this.thread = NULL_THREAD_REF;
}
public NoodleDebug(Activity activity,@IdRes int textViewId) {
this(activity, activity.findViewById(textViewId));
}
public NoodleDebug(Activity activity, TextView textView) {
this.activity = Objects.requireNonNull(activity);
this.textView = Objects.requireNonNull(textView);
this.tokens = new LinkedList<>();
this.debug = new StringBuilder(64);
this.thread = NULL_THREAD_REF;
}
public NoodleDebug bind() {
synchronized (this.tokens) {
Thread thread;
if ((thread = this.thread.get()) != null) {
Log.e(TAG, "Trying to bind to thread \"" + Thread.currentThread().getName() +
"\" while already bound to \"" + thread.getName() + "\"");
return NULL;
}
this.tokens.clear();
}
if (this.enabled) {
this.thread = new WeakReference<>(Thread.currentThread());
THREAD_NOODLE.set(this);
} else {
this.thread = NULL_THREAD_REF;
THREAD_NOODLE.remove();
}
return this;
}
public void unbind() {
this.thread = NULL_THREAD_REF;
boolean markDirty;
synchronized (this.tokens) {
markDirty = !this.tokens.isEmpty();
this.tokens.clear();
}
if (markDirty) this.markDirty();
}
public boolean isBound() {
return this.thread.get() != null;
}
public void push(String token) {
if (!this.enabled) return;
synchronized (this.tokens) {
this.tokens.add(token);
}
if (!token.isEmpty())
this.markDirty();
}
public void pop() {
if (!this.enabled) return;
String last;
synchronized (this.tokens) {
last = this.tokens.removeLast();
}
if (!last.isEmpty())
this.markDirty();
}
public void replace(String token) {
if (!this.enabled) return;
String last;
synchronized (this.tokens) {
last = this.tokens.removeLast();
this.tokens.add(token);
}
if (!last.equals(token))
this.markDirty();
}
public void setEnabled(boolean enabled) {
if (this.enabled && !enabled) {
this.thread = NULL_THREAD_REF;
synchronized (this.tokens) {
this.tokens.clear();
}
this.markDirty();
}
this.enabled = enabled;
}
protected void markDirty() {
assert this.activity != null;
assert this.textView != null;
if (this.updating) return;
this.updating = true;
this.activity.runOnUiThread(() -> {
String debugText;
synchronized (this.tokens) {
StringBuilder debug = this.debug;
debug.setLength(0);
boolean first = true;
for (String text : this.tokens) {
if (text.isEmpty()) continue;
if (first) first = false;
else debug.append(" > ");
debug.append(text);
}
debugText = debug.toString();
}
this.updating = false;
this.textView.setText(debugText);
});
}
@NonNull
public static NoodleDebug getNoodleDebug() {
NoodleDebug noodleDebug = THREAD_NOODLE.get();
if (noodleDebug == null) return NULL;
if (noodleDebug.thread.get() != Thread.currentThread() ||
noodleDebug.activity.isDestroyed()) {
THREAD_NOODLE.remove();
return NULL;
}
return noodleDebug;
}
}

@ -0,0 +1,25 @@
package com.fox2code.mmm.utils;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import com.fox2code.mmm.MainActivity;
import java.util.concurrent.ThreadLocalRandom;
public enum ProcessHelper {
;
private static final int sPendingIntentId = ThreadLocalRandom.current().nextInt(100, 1000000 + 1);
public static void restartApplicationProcess(Context context) {
Intent mStartActivity = new Intent(context, MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent mPendingIntent = PendingIntent.getActivity(context, sPendingIntentId,
mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager mgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
System.exit(0); // Exit app process
}
}

@ -60,10 +60,6 @@ public abstract class SyncManager {
}
}
public final boolean isRepoUpdating() {
return this.syncing;
}
public final void afterUpdate() {
if (this.syncing) synchronized (this.syncLock) { Thread.yield(); }
}

@ -0,0 +1,34 @@
package com.fox2code.mmm.utils
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.MainApplication.ReleaseTree
import io.sentry.Sentry
import io.sentry.SentryLevel
import io.sentry.android.timber.SentryTimberTree
import timber.log.Timber
import timber.log.Timber.Forest.plant
@Suppress("UnstableApiUsage")
object TimberUtils {
@JvmStatic
fun configTimber() {
// init timber
// init timber
if (BuildConfig.DEBUG) {
plant(Timber.DebugTree())
} else {
if (MainApplication.isCrashReportingEnabled()) {
plant(
SentryTimberTree(
Sentry.getCurrentHub(),
SentryLevel.ERROR,
SentryLevel.ERROR
)
)
} else {
plant(ReleaseTree())
}
}
}
}

@ -0,0 +1,186 @@
package com.fox2code.mmm.utils;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.R;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.PropUtils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import timber.log.Timber;
public class ZipFileOpener extends FoxActivity {
AlertDialog loading = null;
// Adds us as a handler for zip files, so we can pass them to the installer
// We should have a content uri provided to us.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loading = BudgetProgressDialog.build(this, R.string.loading, R.string.zip_unpacking);
new Thread(() -> {
Timber.d("onCreate: %s", getIntent());
File zipFile;
Uri uri = getIntent().getData();
if (uri == null) {
Timber.e("onCreate: No data provided");
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
return;
}
// Try to copy the file to our cache
try {
// check if its a file over 10MB
Long fileSize = Files.getFileSize(this, uri);
if (fileSize == null) fileSize = 0L;
if (1000L * 1000 * 10 < fileSize) {
runOnUiThread(() -> loading.show());
}
zipFile = File.createTempFile("module", ".zip", getCacheDir());
try (InputStream inputStream = getContentResolver().openInputStream(uri); FileOutputStream outputStream = new FileOutputStream(zipFile)) {
if (inputStream == null) {
Timber.e("onCreate: Failed to open input stream");
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
return;
}
byte[] buffer = new byte[4096];
int read;
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
}
} catch (
Exception e) {
Timber.e(e, "onCreate: Failed to copy zip file");
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
return;
}
// Ensure zip is not empty
if (zipFile.length() == 0) {
Timber.e("onCreate: Zip file is empty");
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
return;
} else {
Timber.d("onCreate: Zip file is " + zipFile.length() + " bytes");
}
ZipEntry entry;
ZipFile zip = null;
// Unpack the zip to validate it's a valid magisk module
// It needs to have, at the bare minimum, a module.prop file. Everything else is technically optional.
// First, check if it's a zip file
try {
zip = new ZipFile(zipFile);
if ((entry = zip.getEntry("module.prop")) == null) {
Timber.e("onCreate: Zip file is not a valid magisk module");
if (BuildConfig.DEBUG) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Timber.d("onCreate: Zip file contents: %s", zip.stream().map(ZipEntry::getName).reduce((a, b) -> a + ", " + b).orElse("empty"));
} else {
Timber.d("onCreate: Zip file contents cannot be listed on this version of android");
}
}
runOnUiThread(() -> {
Toast.makeText(this, R.string.invalid_format, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
return;
}
} catch (
Exception e) {
Timber.e(e, "onCreate: Failed to open zip file");
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
if (zip != null) {
try {
zip.close();
} catch (IOException exception) {
Timber.e(Log.getStackTraceString(exception));
}
}
return;
}
Timber.d("onCreate: Zip file is valid");
String moduleInfo;
try {
moduleInfo = PropUtils.readModulePropSimple(zip.getInputStream(entry), "name");
if (moduleInfo == null) {
moduleInfo = PropUtils.readModulePropSimple(zip.getInputStream(entry), "id");
}
if (moduleInfo == null) {
throw new NullPointerException("moduleInfo is null, check earlier logs for root cause");
}
} catch (
Exception e) {
Timber.e(e, "onCreate: Failed to load module id");
runOnUiThread(() -> {
Toast.makeText(this, R.string.zip_prop_load_failed, Toast.LENGTH_LONG).show();
finishAndRemoveTask();
});
try {
zip.close();
} catch (IOException exception) {
Timber.e(Log.getStackTraceString(exception));
}
return;
}
try {
zip.close();
} catch (IOException exception) {
Timber.e(Log.getStackTraceString(exception));
}
String finalModuleInfo = moduleInfo;
runOnUiThread(() -> {
new MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.zip_security_warning, finalModuleInfo))
.setMessage(getString(R.string.zip_intent_module_install, finalModuleInfo, Files.getFileName(this, uri)))
.setCancelable(false)
.setNegativeButton(R.string.no, (d, i) -> {
d.dismiss();
finishAndRemoveTask();
})
.setPositiveButton(R.string.yes, (d, i) -> {
d.dismiss();
// Pass the file to the installer
FoxActivity compatActivity = FoxActivity.getFoxActivity(this);
IntentHelper.openInstaller(compatActivity, zipFile.getAbsolutePath(),
compatActivity.getString(
R.string.local_install_title), null, null, false,
BuildConfig.DEBUG && // Use debug mode if no root
InstallerInitializer.peekMagiskPath() == null);
finish();
})
.show();
loading.dismiss();
});
}).start();
}
}

@ -0,0 +1,283 @@
package com.fox2code.mmm.utils.io;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.OpenableColumns;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.fox2code.mmm.MainApplication;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import com.topjohnwu.superuser.io.SuFileOutputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.io.FileUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import timber.log.Timber;
/** @noinspection ResultOfMethodCallIgnored*/
public enum Files {
;
private static final boolean is64bit = Build.SUPPORTED_64_BIT_ABIS.length > 0;
// stolen from https://stackoverflow.com/a/25005243
public static @NonNull String getFileName(Context context, Uri uri) {
String result = null;
if (Objects.equals(uri.getScheme(), "content")) {
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (index != -1) {
result = cursor.getString(index);
}
}
}
}
if (result == null) {
result = uri.getPath();
int cut = Objects.requireNonNull(result).lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
return result;
}
// based on https://stackoverflow.com/a/63018108
public static @Nullable Long getFileSize(Context context, Uri uri) {
Long result = null;
try {
String scheme = uri.getScheme();
if (Objects.equals(scheme, "content")) {
Cursor returnCursor = context.getContentResolver().
query(uri, null, null, null, null);
int sizeIndex = Objects.requireNonNull(returnCursor).getColumnIndex(OpenableColumns.SIZE);
returnCursor.moveToFirst();
long size = returnCursor.getLong(sizeIndex);
returnCursor.close();
result = size;
}
if (Objects.equals(scheme, "file")) {
result = new File(Objects.requireNonNull(uri.getPath())).length();
}
} catch (Exception e) {
Timber.e(Log.getStackTraceString(e));
return result;
}
return result;
}
public static void write(File file, byte[] bytes) throws IOException {
// make the dir if necessary
Objects.requireNonNull(file.getParentFile()).mkdirs();
try (OutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(bytes);
outputStream.flush();
}
}
public static byte[] read(File file) throws IOException {
try (InputStream inputStream = new FileInputStream(file)) {
return readAllBytes(inputStream);
}
}
public static void writeSU(File file, byte[] bytes) throws IOException {
// make the dir if necessary
Objects.requireNonNull(file.getParentFile()).mkdirs();
try (OutputStream outputStream = SuFileOutputStream.open(file)) {
outputStream.write(bytes);
outputStream.flush();
}
}
public static byte[] readSU(File file) throws IOException {
if (file.isFile() && file.canRead()) {
try { // Read as app if su not required
return read(file);
} catch (IOException ignored) {
}
}
try (InputStream inputStream = SuFileInputStream.open(file)) {
return readAllBytes(inputStream);
}
}
public static boolean existsSU(File file) {
return file.exists() || new SuFile(file.getAbsolutePath()).exists();
}
public static void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
int nRead;
byte[] data = new byte[16384];
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
outputStream.write(data, 0, nRead);
}
outputStream.flush();
}
public static void closeSilently(Closeable closeable) {
try {
if (closeable != null) closeable.close();
} catch (IOException ignored) {
}
}
public static ByteArrayOutputStream makeBuffer(long capacity) {
// Cap buffer to 1 Gib (or 512 Mib for 32bit) to avoid memory errors
return Files.makeBuffer((int) Math.min(capacity, is64bit ? 0x40000000 : 0x20000000));
}
public static ByteArrayOutputStream makeBuffer(int capacity) {
return new ByteArrayOutputStream(Math.max(0x20, capacity)) {
@NonNull
@Override
public byte[] toByteArray() {
return this.buf.length == this.count ?
this.buf : super.toByteArray();
}
};
}
public static byte[] readAllBytes(InputStream inputStream) throws IOException {
ByteArrayOutputStream buffer = Files.makeBuffer(inputStream.available());
copy(inputStream, buffer);
return buffer.toByteArray();
}
public static void fixJavaZipHax(byte[] bytes) {
if (bytes.length > 8 && bytes[0x6] == 0x0 && bytes[0x7] == 0x0 && bytes[0x8] == 0x8)
bytes[0x7] = 0x8; // Known hax to prevent java zip file read
}
public static void patchModuleSimple(byte[] bytes, OutputStream outputStream) throws IOException {
fixJavaZipHax(bytes);
patchModuleSimple(new ByteArrayInputStream(bytes), outputStream);
}
public static void patchModuleSimple(InputStream inputStream, OutputStream outputStream) throws IOException {
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream);
int nRead;
byte[] data = new byte[16384];
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
String name = zipEntry.getName();
int i = name.indexOf('/', 1);
if (i == -1) continue;
String newName = name.substring(i + 1);
if (newName.startsWith(".git")) continue; // Skip metadata
zipOutputStream.putNextEntry(new ZipEntry(newName));
while ((nRead = zipInputStream.read(data, 0, data.length)) != -1) {
zipOutputStream.write(data, 0, nRead);
}
zipOutputStream.flush();
zipOutputStream.closeEntry();
zipInputStream.closeEntry();
}
zipOutputStream.finish();
zipOutputStream.flush();
zipOutputStream.close();
zipInputStream.close();
}
public static void fixSourceArchiveShit(byte[] rawModule) {
// unzip the module, check if it has just one folder within. if so, switch to the folder and zip up contents, and replace the original file with that
try {
File tempDir = new File(MainApplication.getINSTANCE().getCacheDir(), "temp");
if (tempDir.exists()) {
FileUtils.deleteDirectory(tempDir);
}
if (!tempDir.mkdirs()) {
throw new IOException("Unable to create temp dir");
}
File tempFile = new File(tempDir, "module.zip");
Files.write(tempFile, rawModule);
File tempUnzipDir = new File(tempDir, "unzip");
if (!tempUnzipDir.mkdirs()) {
throw new IOException("Unable to create temp unzip dir");
}
// unzip
Timber.d("Unzipping module to %s", tempUnzipDir.getAbsolutePath());
try (ZipFile zipFile = new ZipFile(tempFile)) {
Enumeration<ZipArchiveEntry> files = zipFile.getEntries();
// check if there is only one folder in the top level
int folderCount = 0;
while (files.hasMoreElements()) {
ZipArchiveEntry entry = files.nextElement();
if (entry.isDirectory()) {
folderCount++;
}
}
if (folderCount == 1) {
files = zipFile.getEntries();
while (files.hasMoreElements()) {
ZipArchiveEntry entry = files.nextElement();
if (entry.isDirectory()) {
continue;
}
File file = new File(tempUnzipDir, entry.getName());
if (!Objects.requireNonNull(file.getParentFile()).exists()) {
if (!file.getParentFile().mkdirs()) {
throw new IOException("Unable to create parent dir");
}
}
try (ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(file)) {
zipArchiveOutputStream.putArchiveEntry(entry);
try (InputStream inputStream = zipFile.getInputStream(entry)) {
copy(inputStream, zipArchiveOutputStream);
}
zipArchiveOutputStream.closeArchiveEntry();
}
}
// zip up the contents of the folder but not the folder itself
File[] filesInFolder = Objects.requireNonNull(tempUnzipDir.listFiles());
// create a new zip file
try (ZipArchiveOutputStream archive = new ZipArchiveOutputStream(new FileOutputStream("new.zip"))) {
for (File files2 : filesInFolder) {
// create a new ZipArchiveEntry and add it to the ZipArchiveOutputStream
ZipArchiveEntry entry = new ZipArchiveEntry(files2, files2.getName());
archive.putArchiveEntry(entry);
try (InputStream input = new FileInputStream(files2)) {
copy(input, archive);
}
archive.closeArchiveEntry();
}
} catch (IOException e) {
Timber.e(e, "Unable to zip up module");
}
} else {
Timber.d("Module does not have a single folder in the top level, skipping");
}
} catch (IOException e) {
Timber.e(e, "Unable to unzip module");
}
} catch (IOException e) {
Timber.e(e, "Unable to create temp dir");
}
}
}

@ -15,46 +15,49 @@
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* */
package com.fox2code.mmm.utils;
package com.fox2code.mmm.utils.io;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
/** Open implementation of ProviderInstaller.installIfNeeded
import timber.log.Timber;
/**
* Open implementation of ProviderInstaller.installIfNeeded
* (Compatible with MicroG even without signature spoofing)
*/
// Note: This code is MIT because I took it from another unpublished project I had
// I might upstream this to MicroG at some point
public class GMSProviderInstaller {
private static final String TAG = "GMSProviderInstaller";
public enum GMSProviderInstaller {
;
private static boolean called = false;
public static void installIfNeeded(final Context context) {
if (context == null) {
throw new NullPointerException("Context must not be null");
}
if (called) return;
if (called)
return;
called = true;
try {
// Trust default GMS implementation
Context remote = context.createPackageContext("com.google.android.gms",
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
Class<?> cl = remote.getClassLoader().loadClass(
"com.google.android.gms.common.security.ProviderInstallerImpl");
Context remote = context.createPackageContext("com.google.android.gms", Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
Class<?> cl = remote.getClassLoader().loadClass("com.google.android.gms.common.security.ProviderInstallerImpl");
cl.getDeclaredMethod("insertProvider", Context.class).invoke(null, remote);
Log.i(TAG, "Installed GMS security providers!");
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "No GMS Implementation are installed on this device");
} catch (Exception e) {
Log.w(TAG, "Failed to install the provider of the current GMS Implementation", e);
Timber.i("Installed GMS security providers!");
} catch (
PackageManager.NameNotFoundException e) {
Timber.w("No GMS Implementation are installed on this device");
} catch (
Exception e) {
Timber.w(e);
}
}
}

@ -0,0 +1,129 @@
package com.fox2code.mmm.utils.io;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import java.util.regex.Pattern;
import timber.log.Timber;
public enum Hashes {
;
private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
private static final Pattern nonAlphaNum = Pattern.compile("[^a-zA-Z0-9]");
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return String.valueOf(hexChars);
}
public static String hashMd5(byte[] input) {
throw new SecurityException("MD5 is not secure");
}
public static String hashSha1(byte[] input) {
throw new SecurityException("SHA-1 is not secure");
}
public static String hashSha256(byte[] input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return bytesToHex(md.digest(input));
} catch (
NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String hashSha512(byte[] input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
return bytesToHex(md.digest(input));
} catch (
NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* Check if the checksum match a file by picking the correct
* hashing algorithm depending on the length of the checksum
*/
public static boolean checkSumMatch(byte[] data, String checksum) {
String hash;
if (checksum == null)
return false;
switch (checksum.length()) {
case 0:
return true; // No checksum
case 32:
hash = Hashes.hashMd5(data);
break;
case 40:
hash = Hashes.hashSha1(data);
break;
case 64:
hash = Hashes.hashSha256(data);
break;
case 128:
hash = Hashes.hashSha512(data);
break;
default:
Timber.e("No hash algorithm for " + checksum.length() * 8 + "bit checksums");
return false;
}
Timber.i("Checksum result (data: " + hash + ",expected: " + checksum + ")");
return hash.equals(checksum.toLowerCase(Locale.ROOT));
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean checkSumValid(String checksum) {
if (checksum == null)
return false;
switch (checksum.length()) {
case 0:
default:
return false;
case 32:
case 40:
case 64:
case 128:
final int len = checksum.length();
for (int i = 0; i < len; i++) {
char c = checksum.charAt(i);
if (c < '0' || c > 'f')
return false;
if (c > '9' && // Easier working with bits
(c & 0b01011111) < 'A')
return false;
}
return true;
}
}
public static String checkSumName(String checksum) {
if (checksum == null)
return null;
return switch (checksum.length()) {
default -> null;
case 32 -> "MD5";
case 40 -> "SHA-1";
case 64 -> "SHA-256";
case 128 -> "SHA-512";
};
}
public static String checkSumFormat(String checksum) {
if (checksum == null)
return null;
// Remove all non-alphanumeric characters
return nonAlphaNum.matcher(checksum.trim()).replaceAll("");
}
}

@ -1,11 +1,10 @@
package com.fox2code.mmm.utils;
package com.fox2code.mmm.utils.io;
import static com.fox2code.mmm.AppUpdateManager.FLAG_COMPAT_LOW_QUALITY;
import static com.fox2code.mmm.AppUpdateManager.getFlagsForModule;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.manager.ModuleInfo;
@ -21,11 +20,16 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
public class PropUtils {
import timber.log.Timber;
@SuppressWarnings("SpellCheckingInspection")
public enum PropUtils {
;
private static final HashMap<String, String> moduleSupportsFallbacks = new HashMap<>();
private static final HashMap<String, String> moduleConfigsFallbacks = new HashMap<>();
private static final HashMap<String, Integer> moduleMinApiFallbacks = new HashMap<>();
private static final HashMap<String, String> moduleUpdateJsonFallbacks = new HashMap<>();
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private static final HashSet<String> moduleMTTRebornFallback = new HashSet<>();
private static final HashSet<String> moduleImportantProp = new HashSet<>(Arrays.asList(
"id", "name", "version", "versionCode"
@ -119,6 +123,10 @@ public class PropUtils {
continue;
String key = line.substring(0, index);
String value = line.substring(index + 1).trim();
// check if field is defined on the moduleInfo object we are reading
if (moduleInfo.toString().contains(key)) {
continue;
}
// name and id have their own implementation
if (isInvalidValue(key)) {
if (local) {
@ -141,7 +149,8 @@ public class PropUtils {
if (local) {
invalid = true;
break;
} throw new IOException("Invalid module id!");
}
throw new IOException("Invalid module id!");
}
readId = true;
if (!moduleInfo.id.equals(value)) {
@ -164,7 +173,8 @@ public class PropUtils {
if (local) {
invalid = true;
break;
} throw new IOException("Invalid module name!");
}
throw new IOException("Invalid module name!");
}
readName = true;
moduleInfo.name = value;
@ -328,7 +338,7 @@ public class PropUtils {
}
}
public static String readModuleId(InputStream inputStream) {
public static String readModulePropSimple(InputStream inputStream, String what) {
if (inputStream == null) return null;
String moduleId = null;
try (BufferedReader bufferedReader = new BufferedReader(
@ -337,16 +347,20 @@ public class PropUtils {
while ((line = bufferedReader.readLine()) != null) {
while (line.startsWith("\u0000"))
line = line.substring(1);
if (line.startsWith("id=")) {
moduleId = line.substring(3).trim();
if (line.startsWith(what + "=")) {
moduleId = line.substring(what.length() + 1).trim();
}
}
} catch (IOException e) {
Log.d("PropUtils", "Failed to get moduleId", e);
Timber.i(e);
}
return moduleId;
}
public static String readModuleId(InputStream inputStream) {
return readModulePropSimple(inputStream, "id");
}
public static void applyFallbacks(ModuleInfo moduleInfo) {
if (moduleInfo.support == null || moduleInfo.support.isEmpty()) {
moduleInfo.support = moduleSupportsFallbacks.get(moduleInfo.id);
@ -372,7 +386,8 @@ public class PropUtils {
|| moduleInfo.author == null || !TextUtils.isGraphic(moduleInfo.author)
|| isNullString(description = moduleInfo.description) || !TextUtils.isGraphic(description)
|| description.toLowerCase(Locale.ROOT).equals(moduleInfo.name.toLowerCase(Locale.ROOT))
|| (getFlagsForModule(moduleInfo.id) & FLAG_COMPAT_LOW_QUALITY) != 0;
|| (getFlagsForModule(moduleInfo.id) & FLAG_COMPAT_LOW_QUALITY) != 0
|| (moduleInfo.id.startsWith("."));
}
private static boolean isInvalidValue(String name) {

@ -0,0 +1,567 @@
package com.fox2code.mmm.utils.io.net;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.system.ErrnoException;
import android.system.Os;
import android.webkit.CookieManager;
import android.webkit.WebSettings;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.webkit.WebViewCompat;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainActivity;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.androidacy.AndroidacyUtil;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.utils.io.Files;
import com.google.android.material.snackbar.Snackbar;
import com.google.net.cronet.okhttptransport.CronetInterceptor;
import org.apache.commons.io.FileUtils;
import org.chromium.net.CronetEngine;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Proxy;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLException;
import okhttp3.Cache;
import okhttp3.Dns;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.dnsoverhttps.DnsOverHttps;
import okhttp3.logging.HttpLoggingInterceptor;
import okio.BufferedSink;
import timber.log.Timber;
public enum Http {
;
private static final OkHttpClient httpClient;
private static final OkHttpClient httpClientDoH;
private static final OkHttpClient httpClientWithCache;
private static final OkHttpClient httpClientWithCacheDoH;
private static final FallBackDNS fallbackDNS;
private static final String androidacyUA;
private static final boolean hasWebView;
private static String needCaptchaAndroidacyHost;
private static boolean doh;
private static boolean urlFactoryInstalled;
static {
MainApplication mainApplication = MainApplication.getINSTANCE();
if (mainApplication == null) {
Error error = new Error("Initialized Http too soon!");
error.fillInStackTrace();
Timber.e(error, "Initialized Http too soon!");
System.out.flush();
System.err.flush();
try {
Os.kill(Os.getpid(), 9);
} catch (ErrnoException e) {
System.exit(9);
}
throw error;
}
CookieManager cookieManager;
try {
cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.flush(); // Make sure the instance work
} catch (Exception t) {
cookieManager = null;
Timber.e(t, "No WebView support!");
// show a toast
Context context = mainApplication.getApplicationContext();
MainActivity.getFoxActivity(context).runOnUiThread(() -> Toast.makeText(mainApplication, R.string.error_creating_cookie_database, Toast.LENGTH_LONG).show());
}
// get webview version
String webviewVersion = "0.0.0";
PackageInfo pi = WebViewCompat.getCurrentWebViewPackage(mainApplication);
if (pi != null) {
webviewVersion = pi.versionName;
}
// webviewVersionMajor is the everything before the first dot
int webviewVersionCode;
// parse webview version
// get the first dot
int dot = webviewVersion.indexOf('.');
if (dot == -1) {
// no dot, use the whole string
webviewVersionCode = Integer.parseInt(webviewVersion);
} else {
// use the first dot
webviewVersionCode = Integer.parseInt(webviewVersion.substring(0, dot));
}
Timber.d("Webview version: %s (%d)", webviewVersion, webviewVersionCode);
hasWebView = cookieManager != null && webviewVersionCode >= 83; // 83 is the first version Androidacy supports due to errors in 82
OkHttpClient.Builder httpclientBuilder = new OkHttpClient.Builder();
// Default is 10, extend it a bit for slow mobile connections.
httpclientBuilder.connectTimeout(5, TimeUnit.SECONDS);
httpclientBuilder.writeTimeout(10, TimeUnit.SECONDS);
httpclientBuilder.readTimeout(15, TimeUnit.SECONDS);
httpclientBuilder.proxy(Proxy.NO_PROXY); // Do not use system proxy
Dns dns = Dns.SYSTEM;
try {
InetAddress[] cloudflareBootstrap = new InetAddress[]{InetAddress.getByName("162.159.36.1"), InetAddress.getByName("162.159.46.1"), InetAddress.getByName("1.1.1.1"), InetAddress.getByName("1.0.0.1"), InetAddress.getByName("162.159.132.53"), InetAddress.getByName("2606:4700:4700::1111"), InetAddress.getByName("2606:4700:4700::1001"), InetAddress.getByName("2606:4700:4700::0064"), InetAddress.getByName("2606:4700:4700::6400")};
dns = s -> {
if ("cloudflare-dns.com".equals(s)) {
return Arrays.asList(cloudflareBootstrap);
}
return Dns.SYSTEM.lookup(s);
};
httpclientBuilder.dns(dns);
WebkitCookieManagerProxy cookieJar = new WebkitCookieManagerProxy();
httpclientBuilder.cookieJar(cookieJar);
dns = new DnsOverHttps.Builder().client(httpclientBuilder.build()).url(Objects.requireNonNull(HttpUrl.parse("https://cloudflare-dns.com/dns-query"))).bootstrapDnsHosts(cloudflareBootstrap).resolvePrivateAddresses(true).build();
} catch (UnknownHostException | RuntimeException e) {
Timber.e(e, "Failed to init DoH");
}
// User-Agent format was agreed on telegram
if (hasWebView) {
androidacyUA = WebSettings.getDefaultUserAgent(mainApplication).replace("wv", "") + " FoxMMM/" + BuildConfig.VERSION_CODE;
} else {
androidacyUA = "Mozilla/5.0 (Linux; Android " + Build.VERSION.RELEASE + "; " + Build.DEVICE + ")" + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Mobile Safari/537.36" + " FoxMmm/" + BuildConfig.VERSION_CODE;
}
httpclientBuilder.addInterceptor(chain -> {
Request.Builder request = chain.request().newBuilder();
request.header("Upgrade-Insecure-Requests", "1");
String host = chain.request().url().host();
if (host.endsWith(".androidacy.com")) {
request.header("User-Agent", androidacyUA);
} else if (!(host.equals("github.com") || host.endsWith(".github.com") || host.endsWith(".jsdelivr.net") || host.endsWith(".githubusercontent.com"))) {
if (InstallerInitializer.peekMagiskPath() != null) {
request.header("User-Agent", // Declare Magisk version to the server
"Magisk/" + InstallerInitializer.peekMagiskVersion());
}
}
if (chain.request().header("Accept-Language") == null) {
request.header("Accept-Language", // Send system language to the server
mainApplication.getResources().getConfiguration().getLocales().get(0).toLanguageTag());
}
// add client hints
request.header("Sec-CH-UA", androidacyUA);
request.header("Sec-CH-UA-Mobile", "?1");
request.header("Sec-CH-UA-Platform", "Android");
request.header("Sec-CH-UA-Platform-Version", Build.VERSION.RELEASE);
request.header("Sec-CH-UA-Arch", Build.SUPPORTED_ABIS[0]);
request.header("Sec-CH-UA-Full-Version", BuildConfig.VERSION_NAME);
request.header("Sec-CH-UA-Model", Build.DEVICE);
request.header("Sec-CH-UA-Bitness", Build.SUPPORTED_64_BIT_ABIS.length > 0 ? "64" : "32");
return chain.proceed(request.build());
});
// for debug builds, add a logging interceptor
// this spams the logcat, so it's disabled by default and hidden behind a build config flag
if (BuildConfig.DEBUG && BuildConfig.DEBUG_HTTP) {
Timber.w("HTTP logging is enabled. Performance may be impacted.");
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
httpclientBuilder.addInterceptor(loggingInterceptor);
}
// Add cronet interceptor
// init cronet
try {
// Load the cronet library
CronetEngine.Builder builder = new CronetEngine.Builder(mainApplication.getApplicationContext());
builder.enableBrotli(true);
builder.enableHttp2(true);
builder.enableQuic(true);
// Cache size is 10MB
// Make the directory if it does not exist
File cacheDir = new File(mainApplication.getCacheDir(), "cronet");
if (!cacheDir.exists()) {
if (!cacheDir.mkdirs()) {
throw new IOException("Failed to create cronet cache directory");
}
}
builder.setStoragePath(mainApplication.getCacheDir().getAbsolutePath() + "/cronet");
builder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 10 * 1024 * 1024);
// Add quic hint
builder.addQuicHint("github.com", 443, 443);
builder.addQuicHint("githubusercontent.com", 443, 443);
builder.addQuicHint("jsdelivr.net", 443, 443);
builder.addQuicHint("androidacy.com", 443, 443);
builder.addQuicHint("sentry.io", 443, 443);
CronetEngine engine = builder.build();
httpclientBuilder.addInterceptor(CronetInterceptor.newBuilder(engine).build());
} catch (Exception e) {
Timber.e(e, "Failed to init cronet");
// Gracefully fallback to okhttp
}
// Fallback DNS cache responses in case request fail but already succeeded once in the past
fallbackDNS = new FallBackDNS(mainApplication, dns, "github.com", "api.github.com", "raw.githubusercontent.com", "camo.githubusercontent.com", "user-images.githubusercontent.com", "cdn.jsdelivr.net", "img.shields.io", "magisk-modules-repo.github.io", "www.androidacy.com", "api.androidacy.com", "production-api.androidacy.com");
httpclientBuilder.dns(Dns.SYSTEM);
httpClient = followRedirects(httpclientBuilder, true).build();
followRedirects(httpclientBuilder, false).build();
httpclientBuilder.dns(fallbackDNS);
httpClientDoH = followRedirects(httpclientBuilder, true).build();
followRedirects(httpclientBuilder, false).build();
httpclientBuilder.cache(new Cache(new File(mainApplication.getCacheDir(), "http_cache"), 16L * 1024L * 1024L)); // 16Mib of cache
httpclientBuilder.dns(Dns.SYSTEM);
httpClientWithCache = followRedirects(httpclientBuilder, true).build();
httpclientBuilder.dns(fallbackDNS);
httpClientWithCacheDoH = followRedirects(httpclientBuilder, true).build();
Timber.i("Initialized Http successfully!");
doh = MainApplication.isDohEnabled();
}
private static OkHttpClient.Builder followRedirects(OkHttpClient.Builder builder, boolean followRedirects) {
return builder.followRedirects(followRedirects).followSslRedirects(followRedirects);
}
public static OkHttpClient getHttpClient() {
return doh ? httpClientDoH : httpClient;
}
public static OkHttpClient getHttpClientWithCache() {
return doh ? httpClientWithCacheDoH : httpClientWithCache;
}
private static void checkNeedCaptchaAndroidacy(String url, int errorCode) {
if (errorCode == 403 && AndroidacyUtil.isAndroidacyLink(url)) {
needCaptchaAndroidacyHost = Uri.parse(url).getHost();
}
}
public static boolean needCaptchaAndroidacy() {
return needCaptchaAndroidacyHost != null;
}
public static String needCaptchaAndroidacyHost() {
return needCaptchaAndroidacyHost;
}
public static void markCaptchaAndroidacySolved() {
needCaptchaAndroidacyHost = null;
}
@SuppressLint("RestrictedApi")
@SuppressWarnings("resource")
public static byte[] doHttpGet(String url, boolean allowCache) throws IOException {
if (BuildConfig.DEBUG_HTTP) {
// Log, but set all query parameters values to "****" while keeping the keys
Timber.d("doHttpGet: %s", url.replaceAll("=[^&]*", "=****"));
}
Response response;
try {
response = (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).get().build()).execute();
} catch (IOException e) {
Timber.e(e, "Failed to fetch %s", url.replaceAll("=[^&]*", "=****"));
throw new HttpException(e.getMessage(), 0);
}
if (BuildConfig.DEBUG_HTTP) {
Timber.d("doHttpGet: request executed");
}
// 200/204 == success, 304 == cache valid
if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) {
Timber.e("Failed to fetch " + url.replaceAll("=[^&]*", "=****") + " with code " + response.code());
checkNeedCaptchaAndroidacy(url, response.code());
// If it's a 401, and an androidacy link, it's probably an invalid token
if (response.code() == 401 && AndroidacyUtil.isAndroidacyLink(url)) {
// Regenerate the token
throw new HttpException("Androidacy token is invalid", 401);
}
throw new HttpException(response.code());
}
if (BuildConfig.DEBUG_HTTP) {
Timber.d("doHttpGet: " + url.replaceAll("=[^&]*", "=****") + " succeeded");
}
ResponseBody responseBody = response.body();
// Use cache api if used cached response
if (response.code() == 304) {
response = response.cacheResponse();
if (response != null) responseBody = response.body();
}
if (BuildConfig.DEBUG_HTTP) {
Timber.d("doHttpGet: returning " + responseBody.contentLength() + " bytes");
}
return responseBody.bytes();
}
public static byte[] doHttpPost(String url, String data, boolean allowCache) throws IOException {
return (byte[]) doHttpPostRaw(url, data, allowCache);
}
@SuppressWarnings("resource")
private static Object doHttpPostRaw(String url, String data, boolean allowCache) throws IOException {
Response response;
response = (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).post(JsonRequestBody.from(data)).header("Content-Type", "application/json").build()).execute();
if (response.isRedirect()) {
return response.request().url().uri().toString();
}
// 200/204 == success, 304 == cache valid
if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) {
if (BuildConfig.DEBUG_HTTP)
Timber.e("Failed to fetch " + url + ", code: " + response.code() + ", body: " + response.body().string());
checkNeedCaptchaAndroidacy(url, response.code());
throw new HttpException(response.code());
}
ResponseBody responseBody = response.body();
// Use cache api if used cached response
if (response.code() == 304) {
response = response.cacheResponse();
if (response != null) responseBody = response.body();
}
return responseBody.bytes();
}
public static byte[] doHttpGet(String url, ProgressListener progressListener) throws IOException {
Response response = getHttpClient().newCall(new Request.Builder().url(url).get().build()).execute();
if (response.code() != 200 && response.code() != 204) {
Timber.e("Failed to fetch " + url + ", code: " + response.code());
checkNeedCaptchaAndroidacy(url, response.code());
throw new HttpException(response.code());
}
ResponseBody responseBody = Objects.requireNonNull(response.body());
InputStream inputStream = responseBody.byteStream();
byte[] buff = new byte[1024 * 4];
long downloaded = 0;
long target = responseBody.contentLength();
ByteArrayOutputStream byteArrayOutputStream = Files.makeBuffer(target);
int divider = 1; // Make everything go in an int
while ((target / divider) > (Integer.MAX_VALUE / 2)) {
divider *= 2;
}
final long UPDATE_INTERVAL = 100;
long nextUpdate = System.currentTimeMillis() + UPDATE_INTERVAL;
long currentUpdate;
Timber.i("Target: " + target + " Divider: " + divider);
progressListener.onUpdate(0, (int) (target / divider), false);
while (true) {
int read = inputStream.read(buff);
if (read == -1) break;
byteArrayOutputStream.write(buff, 0, read);
downloaded += read;
currentUpdate = System.currentTimeMillis();
if (nextUpdate < currentUpdate) {
nextUpdate = currentUpdate + UPDATE_INTERVAL;
progressListener.onUpdate((int) (downloaded / divider), (int) (target / divider), false);
}
}
inputStream.close();
progressListener.onUpdate((int) (downloaded / divider), (int) (target / divider), true);
return byteArrayOutputStream.toByteArray();
}
public static void cleanDnsCache() {
if (Http.fallbackDNS != null) {
Http.fallbackDNS.cleanDnsCache();
}
}
public static String getAndroidacyUA() {
return androidacyUA;
}
public static void setDoh(boolean doh) {
Timber.i("DoH: " + Http.doh + " -> " + doh);
Http.doh = doh;
}
public static boolean hasWebView() {
return hasWebView;
}
public static void ensureCacheDirs() {
try {
FileUtils.forceMkdir(new File((MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/wasm").replaceAll("//", "/")));
FileUtils.forceMkdir(new File((MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/js").replaceAll("//", "/")));
FileUtils.forceMkdir(new File((MainApplication.getINSTANCE().getDataDir() + "/cache/cronet").replaceAll("//", "/")));
} catch (IOException e) {
Timber.e("Could not create cache dirs");
}
}
public static void ensureURLHandler(Context context) {
if (!urlFactoryInstalled) {
try {
URL.setURLStreamHandlerFactory(new CronetEngine.Builder(context).build().createURLStreamHandlerFactory());
urlFactoryInstalled = true;
} catch (Error ignored) {
// Ignore
}
}
}
public static boolean hasConnectivity() {
// Check if we have internet connection
Timber.d("Checking internet connection...");
// this url is actually hosted by Cloudflare and is not dependent on Androidacy servers being up
byte[] resp;
try {
resp = Http.doHttpGet("https://production-api.androidacy.com/cdn-cgi/trace", false);
} catch (Exception e) {
Timber.e(e, "Failed to check internet connection. Assuming no internet connection.");
// check if it's a security or ssl exception
if (e instanceof SSLException || e instanceof SecurityException) {
// if it is, user installed a certificate that blocks the connection
// show a snackbar to inform the user
Activity context = MainApplication.getINSTANCE().getLastCompatActivity();
new Handler(Looper.getMainLooper()).post(() -> {
if (context != null) {
Snackbar.make(context.findViewById(android.R.id.content), R.string.certificate_error, Snackbar.LENGTH_LONG).show();
}
});
}
return false;
}
// get the response body
String response = new String(resp, StandardCharsets.UTF_8);
// check if the response body contains "visit_scheme=https" and "http/<some number>"
// if it does, we have internet connection
return response.contains("visit_scheme=https") && response.contains("http/");
}
public interface ProgressListener {
void onUpdate(int downloaded, int total, boolean done);
}
/**
* FallBackDNS store successful DNS request to return them later
* can help make the app to work later when the current DNS system
* isn't functional or available.
* <p>
* Note: DNS Cache is stored in user data.
*/
private static class FallBackDNS implements Dns {
private final Dns parent;
private final SharedPreferences sharedPreferences;
private final HashSet<String> fallbacks;
private final HashMap<String, List<InetAddress>> fallbackCache;
public FallBackDNS(Context context, Dns parent, String... fallbacks) {
this.sharedPreferences = context.getSharedPreferences("mmm_dns", Context.MODE_PRIVATE);
this.parent = parent;
this.fallbacks = new HashSet<>(Arrays.asList(fallbacks));
this.fallbackCache = new HashMap<>();
}
@NonNull
private static String toString(@NonNull List<InetAddress> inetAddresses) {
if (inetAddresses.isEmpty()) return "";
Iterator<InetAddress> inetAddressIterator = inetAddresses.iterator();
StringBuilder stringBuilder = new StringBuilder();
while (true) {
stringBuilder.append(inetAddressIterator.next().getHostAddress());
if (!inetAddressIterator.hasNext()) return stringBuilder.toString();
stringBuilder.append("|");
}
}
@NonNull
private static List<InetAddress> fromString(@NonNull String string) throws UnknownHostException {
if (string.isEmpty()) return Collections.emptyList();
String[] strings = string.split("\\|");
ArrayList<InetAddress> inetAddresses = new ArrayList<>(strings.length);
for (String address : strings) {
inetAddresses.add(InetAddress.getByName(address));
}
return inetAddresses;
}
@NonNull
@Override
public List<InetAddress> lookup(@NonNull String s) throws UnknownHostException {
if (this.fallbacks.contains(s)) {
List<InetAddress> addresses;
synchronized (this.fallbackCache) {
addresses = this.fallbackCache.get(s);
if (addresses != null) return addresses;
try {
addresses = this.parent.lookup(s);
if (addresses.isEmpty() || addresses.get(0).isLoopbackAddress())
throw new UnknownHostException(s);
this.fallbackCache.put(s, addresses);
this.sharedPreferences.edit().putString(s.replace('.', '_'), toString(addresses)).apply();
} catch (UnknownHostException e) {
String key = this.sharedPreferences.getString(s.replace('.', '_'), "");
if (key.isEmpty()) throw e;
try {
addresses = fromString(key);
this.fallbackCache.put(s, addresses);
} catch (UnknownHostException e2) {
this.sharedPreferences.edit().remove(s.replace('.', '_')).apply();
throw e;
}
}
}
return addresses;
} else {
return this.parent.lookup(s);
}
}
void cleanDnsCache() {
synchronized (this.fallbackCache) {
this.fallbackCache.clear();
}
}
}
private static class JsonRequestBody extends RequestBody {
private static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json");
private static final JsonRequestBody EMPTY = new JsonRequestBody(new byte[0]);
final byte[] data;
private JsonRequestBody(byte[] data) {
this.data = data;
}
static JsonRequestBody from(String data) {
if (data == null || data.length() == 0) {
return EMPTY;
}
return new JsonRequestBody(data.getBytes(StandardCharsets.UTF_8));
}
@Nullable
@Override
public MediaType contentType() {
return JSON_MEDIA_TYPE;
}
@Override
public long contentLength() {
return this.data.length;
}
@Override
public void writeTo(@NonNull BufferedSink bufferedSink) throws IOException {
bufferedSink.write(this.data);
}
}
}

@ -0,0 +1,36 @@
package com.fox2code.mmm.utils.io.net;
import androidx.annotation.Keep;
import java.io.IOException;
public final class HttpException extends IOException {
private final int errorCode;
HttpException(String text, int errorCode) {
super(text);
this.errorCode = errorCode;
}
@Keep
public HttpException(int errorCode) {
super("Received error code: " + errorCode);
this.errorCode = errorCode;
}
public int getErrorCode() {
return errorCode;
}
public boolean shouldTimeout() {
return switch (errorCode) {
case 419, 429, 503 -> true;
default -> false;
};
}
public static boolean shouldTimeout(Exception exception) {
return exception instanceof HttpException &&
((HttpException) exception).shouldTimeout();
}
}

@ -0,0 +1,127 @@
package com.fox2code.mmm.utils.io.net;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.CookieStore;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
import timber.log.Timber;
public class WebkitCookieManagerProxy extends CookieManager implements CookieJar {
private final android.webkit.CookieManager webkitCookieManager;
public WebkitCookieManagerProxy() {
this(null, null);
}
WebkitCookieManagerProxy(CookieStore ignoredStore, CookiePolicy cookiePolicy) {
super(null, cookiePolicy);
this.webkitCookieManager = android.webkit.CookieManager.getInstance();
}
@Override
public void put(URI uri, Map<String, List<String>> responseHeaders)
throws IOException {
// make sure our args are valid
if ((uri == null) || (responseHeaders == null))
return;
// save our url once
String url = uri.toString();
// go over the headers
for (String headerKey : responseHeaders.keySet()) {
// ignore headers which aren't cookie related
if ((headerKey == null)
|| !(headerKey.equalsIgnoreCase("Set-Cookie2") || headerKey
.equalsIgnoreCase("Set-Cookie")))
continue;
// process each of the headers
for (String headerValue : Objects.requireNonNull(responseHeaders.get(headerKey))) {
webkitCookieManager.setCookie(url, headerValue);
}
}
}
@Override
public Map<String, List<String>> get(URI uri,
Map<String, List<String>> requestHeaders) throws IOException {
// make sure our args are valid
if ((uri == null) || (requestHeaders == null))
throw new IllegalArgumentException("Argument is null");
// save our url once
String url = uri.toString();
// prepare our response
Map<String, List<String>> res = new java.util.HashMap<>();
// get the cookie
String cookie = webkitCookieManager.getCookie(url);
// return it
if (cookie != null) {
res.put("Cookie", List.of(cookie));
}
return res;
}
@Override
public CookieStore getCookieStore() {
// we don't want anyone to work with this cookie store directly
throw new UnsupportedOperationException();
}
@Override
public void saveFromResponse(@NonNull HttpUrl url, List<Cookie> cookies) {
HashMap<String, List<String>> generatedResponseHeaders = new HashMap<>();
ArrayList<String> cookiesList = new ArrayList<>();
for (Cookie c : cookies) {
// toString correctly generates a normal cookie string
cookiesList.add(c.toString());
}
generatedResponseHeaders.put("Set-Cookie", cookiesList);
try {
put(url.uri(), generatedResponseHeaders);
} catch (IOException e) {
Timber.e(e, "Error adding cookies through okhttp");
}
}
@NonNull
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
ArrayList<Cookie> cookieArrayList = new ArrayList<>();
try {
Map<String, List<String>> cookieList = get(url.uri(), new HashMap<>());
// Format here looks like: "Cookie":["cookie1=val1;cookie2=val2;"]
for (List<String> ls : cookieList.values()) {
for (String s : ls) {
String[] cookies = s.split(";");
for (String cookie : cookies) {
Cookie c = Cookie.parse(url, cookie);
cookieArrayList.add(c);
}
}
}
} catch (IOException e) {
Timber.e(e, "error making cookie!");
}
return cookieArrayList;
}
}

@ -0,0 +1,313 @@
package com.fox2code.mmm.utils.realm;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import io.realm.Realm;
import io.realm.RealmObject;
import io.realm.RealmResults;
import io.realm.annotations.PrimaryKey;
import io.realm.annotations.Required;
import timber.log.Timber;
@SuppressWarnings("unused")
public class ModuleListCache extends RealmObject {
// for compatibility, only id is required
@PrimaryKey
@Required
private String codename;
private String name;
private String version;
private int versionCode;
private String author;
private String description;
private int minApi;
private int maxApi;
private int minMagisk;
private boolean needRamdisk;
private String support;
private String donate;
private String config;
private boolean changeBoot;
private boolean mmtReborn;
private String repoId;
private boolean installed;
private int installedVersionCode;
private int lastUpdate;
// androidacy specific, may be added by other repos
private boolean safe;
private int stats;
public ModuleListCache(String codename, String name, String version, int versionCode, String author, String description, int minApi, int maxApi, int minMagisk, boolean needRamdisk, String support, String donate, String config, boolean changeBoot, boolean mmtReborn, String repoId, boolean installed, int installedVersionCode, int lastUpdate, int stats) {
this.codename = codename;
this.name = name;
this.version = version;
this.versionCode = versionCode;
this.author = author;
this.description = description;
this.minApi = minApi;
this.maxApi = maxApi;
this.minMagisk = minMagisk;
this.needRamdisk = needRamdisk;
this.support = support;
this.donate = donate;
this.config = config;
this.changeBoot = changeBoot;
this.mmtReborn = mmtReborn;
this.repoId = repoId;
this.installed = installed;
this.installedVersionCode = installedVersionCode;
this.lastUpdate = lastUpdate;
this.safe = false;
this.stats = stats;
}
public ModuleListCache() {
}
// get all modules from a repo as a json object
public static JSONObject getRepoModulesAsJson(String repoId) {
Realm realm = Realm.getDefaultInstance();
RealmResults<ModuleListCache> modules = realm.where(ModuleListCache.class).equalTo("repoId", repoId).findAll();
JSONObject jsonObject = new JSONObject();
for (ModuleListCache module : modules) {
try {
jsonObject.put(module.getCodename(), module.toJson());
} catch (
JSONException e) {
Timber.e(e);
}
}
realm.close();
return jsonObject;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public int getVersionCode() {
return versionCode;
}
public void setVersionCode(int versionCode) {
this.versionCode = versionCode;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getSupport() {
return support;
}
public void setSupport(String support) {
this.support = support;
}
public String getDonate() {
return donate;
}
public void setDonate(String donate) {
this.donate = donate;
}
public String getConfig() {
return config;
}
public void setConfig(String config) {
this.config = config;
}
public boolean isChangeBoot() {
return changeBoot;
}
public void setChangeBoot(boolean changeBoot) {
this.changeBoot = changeBoot;
}
public boolean isMmtReborn() {
return mmtReborn;
}
public void setMmtReborn(boolean mmtReborn) {
this.mmtReborn = mmtReborn;
}
public String getRepoId() {
return repoId;
}
public void setRepoId(String repoId) {
this.repoId = repoId;
}
public boolean isInstalled() {
return installed;
}
public void setInstalled(boolean installed) {
this.installed = installed;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public int getMinApi() {
return minApi;
}
public void setMinApi(int minApi) {
this.minApi = minApi;
}
public int getMaxApi() {
return maxApi;
}
public void setMaxApi(int maxApi) {
this.maxApi = maxApi;
}
public int getMinMagisk() {
return minMagisk;
}
public void setMinMagisk(int minMagisk) {
this.minMagisk = minMagisk;
}
public boolean isNeedRamdisk() {
return needRamdisk;
}
public void setNeedRamdisk(boolean needRamdisk) {
this.needRamdisk = needRamdisk;
}
public int getInstalledVersionCode() {
return installedVersionCode;
}
public void setInstalledVersionCode(int installedVersionCode) {
this.installedVersionCode = installedVersionCode;
}
public String getCodename() {
return codename;
}
public void setCodename(String codename) {
this.codename = codename;
}
public int getLastUpdate() {
return lastUpdate;
}
public void setLastUpdate(int lastUpdate) {
this.lastUpdate = lastUpdate;
}
public boolean isSafe() {
return safe;
}
public void setSafe(boolean safe) {
this.safe = safe;
}
public int getStats() {
return stats;
}
public void setStats(int stats) {
this.stats = stats;
}
private JSONObject toJson() {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("name", name);
jsonObject.put("version", version);
jsonObject.put("versionCode", versionCode);
jsonObject.put("author", author);
jsonObject.put("description", description);
jsonObject.put("minApi", minApi);
jsonObject.put("maxApi", maxApi);
jsonObject.put("minMagisk", minMagisk);
jsonObject.put("needRamdisk", needRamdisk);
jsonObject.put("support", support);
jsonObject.put("donate", donate);
jsonObject.put("config", config);
jsonObject.put("changeBoot", changeBoot);
jsonObject.put("mmtReborn", mmtReborn);
jsonObject.put("repoId", repoId);
jsonObject.put("installed", installed);
jsonObject.put("installedVersionCode", installedVersionCode);
jsonObject.put("lastUpdate", lastUpdate);
jsonObject.put("safe", safe);
jsonObject.put("stats", stats);
} catch (JSONException e) {
e.printStackTrace();
}
return jsonObject;
}
public RealmResults<ModuleListCache> getModules() {
// return all modules matching the repo id
Realm realm = Realm.getDefaultInstance();
RealmResults<ModuleListCache> modules = realm.where(ModuleListCache.class).equalTo("repoId", repoId).findAll();
realm.close();
return modules;
}
// same as above but returns a json object
public JSONObject getModulesAsJson(String repoId) {
Realm realm = Realm.getDefaultInstance();
RealmResults<ModuleListCache> modules = realm.where(ModuleListCache.class).equalTo("repoId", repoId).findAll();
JSONObject jsonObject = new JSONObject();
// everything goes under top level "modules" key
try {
jsonObject.put("modules", new JSONArray());
} catch (JSONException ignored) {
// we should never get here
}
for (ModuleListCache module : modules) {
try {
jsonObject.getJSONArray("modules").put(module.toJson());
} catch (
JSONException e) {
Timber.e(e);
}
}
realm.close();
return jsonObject;
}
}

@ -0,0 +1,118 @@
package com.fox2code.mmm.utils.realm;
import io.realm.Realm;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
import io.realm.annotations.Required;
@SuppressWarnings("unused")
public class ReposList extends RealmObject {
// Each repo is identified by its id, has a url field, and an enabled field
// there's also an optional donate and support field
@Required
@PrimaryKey
private String id;
@Required
private String url;
private boolean enabled;
private String donate;
private String support;
private String submitModule;
private int lastUpdate;
private String website;
private String name;
public ReposList(String id, String url, boolean enabled, String donate, String support) {
this.id = id;
this.url = url;
this.enabled = enabled;
this.donate = donate;
this.support = support;
this.submitModule = null;
this.lastUpdate = 0;
}
public ReposList() {
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getDonate() {
return donate;
}
public void setDonate(String donate) {
this.donate = donate;
}
public String getSupport() {
return support;
}
public void setSupport(String support) {
this.support = support;
}
// get metadata for a repo
public static ReposList getRepo(String id) {
Realm realm = Realm.getDefaultInstance();
ReposList repo = realm.where(ReposList.class).equalTo("id", id).findFirst();
realm.close();
return repo;
}
public String getSubmitModule() {
return submitModule;
}
public void setSubmitModule(String submitModule) {
this.submitModule = submitModule;
}
public int getLastUpdate() {
return lastUpdate;
}
public void setLastUpdate(int lastUpdate) {
this.lastUpdate = lastUpdate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getWebsite() {
return website;
}
public void setWebsite(String website) {
this.website = website;
}
}

@ -1,4 +1,4 @@
package com.fox2code.mmm.sentry;
package com.fox2code.mmm.utils.sentry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -9,7 +9,7 @@ import io.sentry.Breadcrumb;
import io.sentry.SentryLevel;
public class SentryBreadcrumb {
final Breadcrumb breadcrumb;
public final Breadcrumb breadcrumb;
public SentryBreadcrumb() {
breadcrumb = new Breadcrumb();

@ -0,0 +1,141 @@
package com.fox2code.mmm.utils.sentry;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import com.fox2code.mmm.CrashHandler;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.androidacy.AndroidacyUtil;
import org.matomo.sdk.extra.TrackHelper;
import java.util.Objects;
import io.sentry.Sentry;
import io.sentry.android.core.SentryAndroid;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.android.timber.SentryTimberIntegration;
import timber.log.Timber;
public class SentryMain {
public static final boolean IS_SENTRY_INSTALLED = true;
public static boolean isCrashing = false;
private static boolean sentryEnabled = false;
/**
* Initialize Sentry
* Sentry is used for crash reporting and performance monitoring.
*/
@SuppressLint({"RestrictedApi", "UnspecifiedImmutableFlag"})
public static void initialize(final MainApplication mainApplication) {
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
isCrashing = true;
TrackHelper.track().exception(throwable).with(MainApplication.getINSTANCE().getTracker());
SharedPreferences.Editor editor = MainApplication.getINSTANCE().getSharedPreferences("sentry", Context.MODE_PRIVATE).edit();
editor.putString("lastExitReason", "crash");
editor.putLong("lastExitTime", System.currentTimeMillis());
editor.putString("lastExitReason", "crash");
editor.putString("lastExitId", String.valueOf(Sentry.getLastEventId()));
editor.apply();
Timber.e("Uncaught exception with sentry ID %s and stacktrace %s", Sentry.getLastEventId(), throwable.getStackTrace());
// open crash handler and exit
Intent intent = new Intent(mainApplication, CrashHandler.class);
// pass the entire exception to the crash handler
intent.putExtra("exception", throwable);
// add stacktrace as string
intent.putExtra("stacktrace", throwable.getStackTrace());
// put lastEventId in intent (get from preferences)
intent.putExtra("lastEventId", String.valueOf(Sentry.getLastEventId()));
// serialize Sentry.captureException and pass it to the crash handler
intent.putExtra("sentryException", throwable);
// pass crashReportingEnabled to crash handler
intent.putExtra("crashReportingEnabled", isSentryEnabled());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
Timber.e("Starting crash handler");
mainApplication.startActivity(intent);
Timber.e("Exiting");
android.os.Process.killProcess(android.os.Process.myPid());
});
// If first_launch pref is not false, refuse to initialize Sentry
SharedPreferences sharedPreferences = MainApplication.getSharedPreferences("mmm");
if (!Objects.equals(sharedPreferences.getString("last_shown_setup", null), "v2")) {
return;
}
sentryEnabled = sharedPreferences.getBoolean("pref_crash_reporting_enabled", false);
// set sentryEnabled on preference change of pref_crash_reporting_enabled
sharedPreferences.registerOnSharedPreferenceChangeListener((sharedPreferences1, s) -> {
if (s.equals("pref_crash_reporting_enabled")) {
sentryEnabled = sharedPreferences1.getBoolean(s, false);
}
});
SentryAndroid.init(mainApplication, options -> {
// If crash reporting is disabled, stop here.
if (!MainApplication.isCrashReportingEnabled()) {
sentryEnabled = false; // Set sentry state to disabled
options.setDsn("");
} else {
// get pref_crash_reporting_pii pref
boolean crashReportingPii = sharedPreferences.getBoolean("crashReportingPii", false);
sentryEnabled = true; // Set sentry state to enabled
options.addIntegration(new FragmentLifecycleIntegration(mainApplication, true, true));
// Enable automatic activity lifecycle breadcrumbs
options.setEnableActivityLifecycleBreadcrumbs(true);
// Enable automatic fragment lifecycle breadcrumbs
options.addIntegration(new SentryTimberIntegration());
options.setCollectAdditionalContext(true);
options.setAttachThreads(true);
options.setAttachStacktrace(true);
options.setEnableNdk(true);
options.addInAppInclude("com.fox2code.mmm");
options.addInAppInclude("com.fox2code.mmm.debug");
options.addInAppInclude("com.fox2code.mmm.fdroid");
options.addInAppExclude("com.fox2code.mmm.utils.sentry.SentryMain");
// Respect user preference for sending PII. default is true on non fdroid builds, false on fdroid builds
options.setSendDefaultPii(crashReportingPii);
options.enableAllAutoBreadcrumbs(true);
// in-app screenshots are only sent if the app crashes, and it only shows the last activity. so no, we won't see your, ahem, "private" stuff
options.setAttachScreenshot(true);
// It just tell if sentry should ping the sentry dsn to tell the app is running. Useful for performance and profiling.
options.setEnableAutoSessionTracking(true);
// disable crash tracking - we handle that ourselves
options.setEnableUncaughtExceptionHandler(false);
// Add a callback that will be used before the event is sent to Sentry.
// With this callback, you can modify the event or, when returning null, also discard the event.
options.setBeforeSend((event, hint) -> {
// in the rare event that crash reporting has been disabled since we started the app, we don't want to send the crash report
if (!sentryEnabled) {
return null;
}
if (isCrashing) {
return null;
}
return event;
});
// Filter breadcrumb content from crash report.
options.setBeforeBreadcrumb((breadcrumb, hint) -> {
String url = (String) breadcrumb.getData("url");
if (url == null || url.isEmpty()) return breadcrumb;
if ("cloudflare-dns.com".equals(Uri.parse(url).getHost())) return null;
if (AndroidacyUtil.isAndroidacyLink(url)) {
breadcrumb.setData("url", AndroidacyUtil.hideToken(url));
}
return breadcrumb;
});
}
});
}
public static void addSentryBreadcrumb(SentryBreadcrumb sentryBreadcrumb) {
if (MainApplication.isCrashReportingEnabled()) {
Sentry.addBreadcrumb(sentryBreadcrumb.breadcrumb);
}
}
@SuppressWarnings("unused")
public static boolean isSentryEnabled() {
return sentryEnabled;
}
}

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="?attr/colorControlNormal" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10 10,-4.5 10,-10S17.5,2 12,2zM4,12c0,-4.4 3.6,-8 8,-8 1.9,0 3.6,0.6 4.9,1.7L5.7,16.9C4.6,15.6 4,13.9 4,12zM12,20c-1.9,0 -3.6,-0.6 -4.9,-1.7L18.3,7.1C19.4,8.5 20,10.2 20,12c0,4.4 -3.6,8 -8,8z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4z"/>
</vector>

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,6.4L17.6,5 12,10.6 6.4,5 5,6.4 10.6,12 5,17.6 6.4,19 12,13.4 17.6,19 19,17.6 13.4,12z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M19.4,10C18.7,6.6 15.6,4 12,4 9.1,4 6.6,5.6 5.4,8 2.3,8.4 0,10.9 0,14c0,3.3 2.7,6 6,6h13c2.8,0 5,-2.2 5,-5 0,-2.6 -2.1,-4.8 -4.7,-5zM17,13l-5,5 -5,-5h3V9h4v4h3z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M18,2H9C7.9,2 7,2.9 7,4v12c0,1.1 0.9,2 2,2h9c1.1,0 2,-0.9 2,-2V4C20,2.9 19.1,2 18,2zM18,16H9V4h9V16zM3,15v-2h2v2H3zM3,9.5h2v2H3V9.5zM10,20h2v2h-2V20zM3,18.5v-2h2v2H3zM5,22c-1.1,0 -2,-0.9 -2,-2h2V22zM8.5,22h-2v-2h2V22zM13.5,22L13.5,22l0,-2h2v0C15.5,21.1 14.6,22 13.5,22zM5,6L5,6l0,2H3v0C3,6.9 3.9,6 5,6z"/>
</vector>

@ -0,0 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path
android:fillColor="@android:color/white" android:pathData="M12,21.4l-1.5,-1.3C5.4,15.4 2,12.3 2,8.5 2,5.4 4.4,3 7.5,3c1.7,0 3.4,0.8 4.5,2.1C13.1,3.8 14.8,3 16.5,3 19.6,3 22,5.4 22,8.5c0,3.8 -3.4,6.9 -8.6,11.5L12,21.4z" />
</vector>

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12.5,14L9,10.5l1.4,-1.4 2.1,2.1L17.6,6 19,7.4 12.5,14zM4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10 10,-4.5 10,-10S17.5,2 12,2zM13.4,18.1L13.4,20h-2.7v-1.9c-1.7,-0.4 -3.2,-1.5 -3.3,-3.4h2c0.1,1.1 0.8,1.9 2.7,1.9 2,0 2.4,-1 2.4,-1.6 0,-0.8 -0.4,-1.6 -2.7,-2.1 -2.5,-0.6 -4.2,-1.6 -4.2,-3.7 0,-1.7 1.4,-2.8 3.1,-3.2L10.7,4h2.7v2c1.9,0.5 2.8,1.9 2.9,3.4L14.3,9.3c-0.1,-1.1 -0.6,-1.9 -2.2,-1.9 -1.5,0 -2.4,0.7 -2.4,1.6 0,0.8 0.7,1.4 2.7,1.9s4.2,1.4 4.2,3.9c0,1.8 -1.4,2.8 -3.1,3.2z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M24,9C20.9,5.9 16.7,4 12,4C7.3,4 3.1,5.9 0,9L12,21v0l0,0L24,9zM2.9,9.1C5.5,7.1 8.7,6 12,6s6.5,1.1 9.1,3.1l-1.4,1.4C17.5,8.9 14.9,8 12,8s-5.5,0.9 -7.7,2.5L2.9,9.1z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M18,16v-5c0,-3.1 -1.6,-5.6 -4.5,-6.3L13.5,4c0,-0.8 -0.7,-1.5 -1.5,-1.5s-1.5,0.7 -1.5,1.5v0.7C7.6,5.4 6,7.9 6,11v5l-2,2v1h16v-1l-2,-2zM13,16h-2v-2h2v2zM13,12h-2L11,8h2v4zM12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M17,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.7,0 -3,-1.3 -3,-3s1.3,-3 3,-3 3,1.3 3,3 -1.3,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M15.5,14h-0.8l-0.3,-0.3C15.4,12.6 16,11.1 16,9.5 16,5.9 13.1,3 9.5,3S3,5.9 3,9.5 5.9,16 9.5,16c1.6,0 3.1,-0.6 4.2,-1.6l0.3,0.3v0.8l5,5L20.5,19l-5,-5zM9.5,14C7,14 5,12 5,9.5S7,5 9.5,5 14,7 14,9.5 12,14 9.5,14z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M17,1L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,3c0,-1.1 -0.9,-2 -2,-2zM17,19L7,19L7,5h10v14zM16,13h-3L13,8h-2v5L8,13l4,4 4,-4z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M16,18v2H8v-2H16zM11,8V16h2V8h3L12,4L8,8H11z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M12,1L3,5v6c0,5.6 3.8,10.7 9,12 5.2,-1.3 9,-6.5 9,-12L21,5l-9,-4zM10,17l-4,-4 1.4,-1.4L10,14.2l6.6,-6.6L18,9l-8,8z"/>
</vector>

@ -5,5 +5,6 @@
android:alpha="0.9"
android:viewportHeight="24"
android:viewportWidth="24">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2z"/>
<path
android:fillColor="@android:color/white" android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10 10,-4.5 10,-10S17.5,2 12,2z" />
</vector>

@ -6,8 +6,8 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
android:pathData="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10C17.5,22 22,17.5 22,12S17.5,2 12,2zM12,20c-4.4,0 -8,-3.6 -8,-8s3.6,-8 8,-8 8,3.6 8,8 -3.6,8 -8,8z" />
<path
android:fillColor="@android:color/white"
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
android:pathData="M12.5,7H11v6l5.3,3.2 0.8,-1.2 -4.5,-2.7z" />
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"/>
android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z" />
</vector>

@ -4,5 +4,5 @@
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path android:fillColor="@android:color/white" android:pathData="M17.6,9.48l1.84,-3.18c0.16,-0.31 0.04,-0.69 -0.26,-0.85c-0.29,-0.15 -0.65,-0.06 -0.83,0.22l-1.88,3.24c-2.86,-1.21 -6.08,-1.21 -8.94,0L5.65,5.67c-0.19,-0.29 -0.58,-0.38 -0.87,-0.2C4.5,5.65 4.41,6.01 4.56,6.3L6.4,9.48C3.3,11.25 1.28,14.44 1,18h22C22.72,14.44 20.7,11.25 17.6,9.48zM7,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25S8.25,13.31 8.25,14C8.25,14.69 7.69,15.25 7,15.25zM17,15.25c-0.69,0 -1.25,-0.56 -1.25,-1.25c0,-0.69 0.56,-1.25 1.25,-1.25s1.25,0.56 1.25,1.25C18.25,14.69 17.69,15.25 17,15.25z"/>
<path android:fillColor="@android:color/white" android:pathData="M17.6,9.5l1.8,-3.2c0.2,-0.3 0,-0.7 -0.3,-0.9c-0.3,-0.2 -0.7,-0.1 -0.8,0.2l-1.9,3.2c-2.9,-1.2 -6.1,-1.2 -8.9,0L5.7,5.7c-0.2,-0.3 -0.6,-0.4 -0.9,-0.2C4.5,5.7 4.4,6 4.6,6.3L6.4,9.5C3.3,11.3 1.3,14.4 1,18h22C22.7,14.4 20.7,11.3 17.6,9.5zM7,15.3c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3S8.3,13.3 8.3,14C8.3,14.7 7.7,15.3 7,15.3zM17,15.3c-0.7,0 -1.3,-0.6 -1.3,-1.3c0,-0.7 0.6,-1.3 1.3,-1.3s1.3,0.6 1.3,1.3C18.3,14.7 17.7,15.3 17,15.3z"/>
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M21.81,12.74l-0.82,-0.63v-0.22l0.8,-0.63c0.16,-0.12 0.2,-0.34 0.1,-0.51l-0.85,-1.48c-0.07,-0.13 -0.21,-0.2 -0.35,-0.2 -0.05,0 -0.1,0.01 -0.15,0.03l-0.95,0.38c-0.08,-0.05 -0.11,-0.07 -0.19,-0.11l-0.15,-1.01c-0.03,-0.21 -0.2,-0.36 -0.4,-0.36h-1.71c-0.2,0 -0.37,0.15 -0.4,0.34l-0.14,1.01c-0.03,0.02 -0.07,0.03 -0.1,0.05l-0.09,0.06 -0.95,-0.38c-0.05,-0.02 -0.1,-0.03 -0.15,-0.03 -0.14,0 -0.27,0.07 -0.35,0.2l-0.85,1.48c-0.1,0.17 -0.06,0.39 0.1,0.51l0.8,0.63v0.23l-0.8,0.63c-0.16,0.12 -0.2,0.34 -0.1,0.51l0.85,1.48c0.07,0.13 0.21,0.2 0.35,0.2 0.05,0 0.1,-0.01 0.15,-0.03l0.95,-0.37c0.08,0.05 0.12,0.07 0.2,0.11l0.15,1.01c0.03,0.2 0.2,0.34 0.4,0.34h1.71c0.2,0 0.37,-0.15 0.4,-0.34l0.15,-1.01c0.03,-0.02 0.07,-0.03 0.1,-0.05l0.09,-0.06 0.95,0.38c0.05,0.02 0.1,0.03 0.15,0.03 0.14,0 0.27,-0.07 0.35,-0.2l0.85,-1.48c0.1,-0.17 0.06,-0.39 -0.1,-0.51zM18,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM17,17h2v4c0,1.1 -0.9,2 -2,2H7c-1.1,0 -2,-0.9 -2,-2V3c0,-1.1 0.9,-2 2,-2h10c1.1,0 2,0.9 2,2v4h-2V6H7v12h10v-1z"/>
android:pathData="M21.8,12.7l-0.8,-0.6v-0.2l0.8,-0.6c0.2,-0.1 0.2,-0.3 0.1,-0.5l-0.9,-1.5c-0.1,-0.1 -0.2,-0.2 -0.4,-0.2 -0.1,0 -0.1,0 -0.2,0l-1,0.4c-0.1,-0.1 -0.1,-0.1 -0.2,-0.1l-0.2,-1c0,-0.2 -0.2,-0.4 -0.4,-0.4h-1.7c-0.2,0 -0.4,0.2 -0.4,0.3l-0.1,1c0,0 -0.1,0 -0.1,0.1l-0.1,0.1 -1,-0.4c-0.1,0 -0.1,0 -0.2,0 -0.1,0 -0.3,0.1 -0.4,0.2l-0.9,1.5c-0.1,0.2 -0.1,0.4 0.1,0.5l0.8,0.6v0.2l-0.8,0.6c-0.2,0.1 -0.2,0.3 -0.1,0.5l0.9,1.5c0.1,0.1 0.2,0.2 0.4,0.2 0.1,0 0.1,0 0.2,0l1,-0.4c0.1,0.1 0.1,0.1 0.2,0.1l0.2,1c0,0.2 0.2,0.3 0.4,0.3h1.7c0.2,0 0.4,-0.2 0.4,-0.3l0.2,-1c0,0 0.1,0 0.1,-0.1l0.1,-0.1 1,0.4c0.1,0 0.1,0 0.2,0 0.1,0 0.3,-0.1 0.4,-0.2l0.9,-1.5c0.1,-0.2 0.1,-0.4 -0.1,-0.5zM18,13.5c-0.8,0 -1.5,-0.7 -1.5,-1.5s0.7,-1.5 1.5,-1.5 1.5,0.7 1.5,1.5 -0.7,1.5 -1.5,1.5zM17,17h2v4c0,1.1 -0.9,2 -2,2H7c-1.1,0 -2,-0.9 -2,-2V3c0,-1.1 0.9,-2 2,-2h10c1.1,0 2,0.9 2,2v4h-2V6H7v12h10v-1z" />
</vector>

@ -4,5 +4,5 @@
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path android:fillColor="@android:color/white" android:pathData="M6,13c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM6,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM6,9c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM3,9.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM6,5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM21,10.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM14,7c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1 -1,0.45 -1,1 0.45,1 1,1zM14,3.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM3,13.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,20.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,3.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM10,7c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1 -1,0.45 -1,1 0.45,1 1,1zM10,12.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM18,13c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,9c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM21,13.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM14,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM14,20.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,8.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM10,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM14,12.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM14,8.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5z"/>
<path android:fillColor="@android:color/white" android:pathData="M6,13c-0.6,0 -1,0.5 -1,1s0.5,1 1,1 1,-0.5 1,-1 -0.5,-1 -1,-1zM6,17c-0.6,0 -1,0.5 -1,1s0.5,1 1,1 1,-0.5 1,-1 -0.5,-1 -1,-1zM6,9c-0.6,0 -1,0.5 -1,1s0.5,1 1,1 1,-0.5 1,-1 -0.5,-1 -1,-1zM3,9.5c-0.3,0 -0.5,0.2 -0.5,0.5s0.2,0.5 0.5,0.5 0.5,-0.2 0.5,-0.5 -0.2,-0.5 -0.5,-0.5zM6,5c-0.6,0 -1,0.5 -1,1s0.5,1 1,1 1,-0.5 1,-1 -0.5,-1 -1,-1zM21,10.5c0.3,0 0.5,-0.2 0.5,-0.5s-0.2,-0.5 -0.5,-0.5 -0.5,0.2 -0.5,0.5 0.2,0.5 0.5,0.5zM14,7c0.6,0 1,-0.5 1,-1s-0.5,-1 -1,-1 -1,0.5 -1,1 0.5,1 1,1zM14,3.5c0.3,0 0.5,-0.2 0.5,-0.5s-0.2,-0.5 -0.5,-0.5 -0.5,0.2 -0.5,0.5 0.2,0.5 0.5,0.5zM3,13.5c-0.3,0 -0.5,0.2 -0.5,0.5s0.2,0.5 0.5,0.5 0.5,-0.2 0.5,-0.5 -0.2,-0.5 -0.5,-0.5zM10,20.5c-0.3,0 -0.5,0.2 -0.5,0.5s0.2,0.5 0.5,0.5 0.5,-0.2 0.5,-0.5 -0.2,-0.5 -0.5,-0.5zM10,3.5c0.3,0 0.5,-0.2 0.5,-0.5s-0.2,-0.5 -0.5,-0.5 -0.5,0.2 -0.5,0.5 0.2,0.5 0.5,0.5zM10,7c0.6,0 1,-0.5 1,-1s-0.5,-1 -1,-1 -1,0.5 -1,1 0.5,1 1,1zM10,12.5c-0.8,0 -1.5,0.7 -1.5,1.5s0.7,1.5 1.5,1.5 1.5,-0.7 1.5,-1.5 -0.7,-1.5 -1.5,-1.5zM18,13c-0.6,0 -1,0.5 -1,1s0.5,1 1,1 1,-0.5 1,-1 -0.5,-1 -1,-1zM18,17c-0.6,0 -1,0.5 -1,1s0.5,1 1,1 1,-0.5 1,-1 -0.5,-1 -1,-1zM18,9c-0.6,0 -1,0.5 -1,1s0.5,1 1,1 1,-0.5 1,-1 -0.5,-1 -1,-1zM18,5c-0.6,0 -1,0.5 -1,1s0.5,1 1,1 1,-0.5 1,-1 -0.5,-1 -1,-1zM21,13.5c-0.3,0 -0.5,0.2 -0.5,0.5s0.2,0.5 0.5,0.5 0.5,-0.2 0.5,-0.5 -0.2,-0.5 -0.5,-0.5zM14,17c-0.6,0 -1,0.5 -1,1s0.5,1 1,1 1,-0.5 1,-1 -0.5,-1 -1,-1zM14,20.5c-0.3,0 -0.5,0.2 -0.5,0.5s0.2,0.5 0.5,0.5 0.5,-0.2 0.5,-0.5 -0.2,-0.5 -0.5,-0.5zM10,8.5c-0.8,0 -1.5,0.7 -1.5,1.5s0.7,1.5 1.5,1.5 1.5,-0.7 1.5,-1.5 -0.7,-1.5 -1.5,-1.5zM10,17c-0.6,0 -1,0.5 -1,1s0.5,1 1,1 1,-0.5 1,-1 -0.5,-1 -1,-1zM14,12.5c-0.8,0 -1.5,0.7 -1.5,1.5s0.7,1.5 1.5,1.5 1.5,-0.7 1.5,-1.5 -0.7,-1.5 -1.5,-1.5zM14,8.5c-0.8,0 -1.5,0.7 -1.5,1.5s0.7,1.5 1.5,1.5 1.5,-0.7 1.5,-1.5 -0.7,-1.5 -1.5,-1.5z"/>
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
android:pathData="M20,8h-2.8c-0.5,-0.8 -1.1,-1.5 -1.8,-2L17,4.4 15.6,3l-2.2,2.2C13,5.1 12.5,5 12,5c-0.5,0 -1,0.1 -1.4,0.2L8.4,3 7,4.4l1.6,1.6C7.9,6.6 7.3,7.2 6.8,8L4,8v2h2.1c-0.1,0.3 -0.1,0.7 -0.1,1v1L4,12v2h2v1c0,0.3 0,0.7 0.1,1L4,16v2h2.8c1,1.8 3,3 5.2,3s4.2,-1.2 5.2,-3L20,18v-2h-2.1c0.1,-0.3 0.1,-0.7 0.1,-1v-1h2v-2h-2v-1c0,-0.3 0,-0.7 -0.1,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
</vector>

@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="?attr/colorControlNormal"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M5,14.5h14v-6L5,8.5v6zM11,0.55L11,3.5h2L13,0.55h-2zM19.04,3.05l-1.79,1.79 1.41,1.41 1.8,-1.79 -1.42,-1.41zM13,22.45L13,19.5h-2v2.95h2zM20.45,18.54l-1.8,-1.79 -1.41,1.41 1.79,1.8 1.42,-1.42zM3.55,4.46l1.79,1.79 1.41,-1.41 -1.79,-1.79 -1.41,1.41zM4.96,19.95l1.79,-1.8 -1.41,-1.41 -1.79,1.79 1.41,1.42z"/>
<path android:fillColor="@android:color/white" android:pathData="M5,14.5h14v-6L5,8.5v6zM11,0.6L11,3.5h2L13,0.6h-2zM19,3.1l-1.8,1.8 1.4,1.4 1.8,-1.8 -1.4,-1.4zM13,22.5L13,19.5h-2v3h2zM20.5,18.5l-1.8,-1.8 -1.4,1.4 1.8,1.8 1.4,-1.4zM3.6,4.5l1.8,1.8 1.4,-1.4 -1.8,-1.8 -1.4,1.4zM5,20l1.8,-1.8 -1.4,-1.4 -1.8,1.8 1.4,1.4z"/>
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4c-1.48,0 -2.85,0.43 -4.01,1.17l1.46,1.46C10.21,6.23 11.08,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3 0,1.13 -0.64,2.11 -1.56,2.62l1.45,1.45C23.16,18.16 24,16.68 24,15c0,-2.64 -2.05,-4.78 -4.65,-4.96zM3,5.27l2.75,2.74C2.56,8.15 0,10.77 0,14c0,3.31 2.69,6 6,6h11.73l2,2L21,20.73 4.27,4 3,5.27zM7.73,10l8,8H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h1.73z"/>
android:pathData="M19.4,10C18.7,6.6 15.6,4 12,4c-1.5,0 -2.9,0.4 -4,1.2l1.5,1.5C10.2,6.2 11.1,6 12,6c3,0 5.5,2.5 5.5,5.5v0.5H19c1.7,0 3,1.3 3,3 0,1.1 -0.6,2.1 -1.6,2.6l1.5,1.5C23.2,18.2 24,16.7 24,15c0,-2.6 -2.1,-4.8 -4.7,-5zM3,5.3l2.8,2.7C2.6,8.2 0,10.8 0,14c0,3.3 2.7,6 6,6h11.7l2,2L21,20.7 4.3,4 3,5.3zM7.7,10l8,8H6c-2.2,0 -4,-1.8 -4,-4s1.8,-4 4,-4h1.7z"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save