Compare commits

..

No commits in common. 'master' and '0.6.7' have entirely different histories.

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

@ -1,94 +1,51 @@
#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:
- '*'
paths-ignore:
- '**.md'
- master
- debug
pull_request:
branches:
- '*'
paths-ignore:
- '**.md'
workflow_dispatch:
branches:
- master
- debug
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- 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
- uses: actions/checkout@v1
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Set Up JDK
uses: actions/setup-java@v1
with:
gradle-home-cache-includes: |
caches
notifications
jdks
${{ github.workspace }}/.gradle/configuration-cache
java-version: 11
- name: Change wrapper permissions
run: chmod +x ./gradlew
# temporary disabled
# - name: Run tests
# run: ./gradlew test
- 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 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
# Upload Artifact Build
# Noted For Output [module-name]/build/outputs/apk
- name: Upload apk debug
uses: actions/upload-artifact@v3
with:
name: FoxMMM-default-x86-debug
path: app/build/outputs/apk/default/debug/*-default-x86-debug.apk
name: FoxMmm-debug
path: app/build/outputs/apk/default/debug/app-default-debug.apk
- name: Upload FoxMMM-default-x86_64-debug
- name: Upload apk fdroid-debug
uses: actions/upload-artifact@v3
with:
name: FoxMMM-default-x86_64-debug
path: app/build/outputs/apk/default/debug/*-default-x86_64-debug.apk
name: FoxMmm-fdroid-debug
path: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk

@ -1,94 +0,0 @@
# 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}}"

@ -1,39 +0,0 @@
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,14 +4,8 @@
/.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

@ -24,7 +24,7 @@ App hiding: I don't intent on hiding the app, the package names should always be
`com.fox2code.mmm` or starts with `com.fox2code.mmm.`, however I notice the presence of
my app is causing issues due to it existing, I may add an hiding feature to the app.
Low quality module filter: Implemented at `com.fox2code.mmm.utils.io.PropUtils.isLowQualityModule`,
Low quality module filter: Implemented at `com.fox2code.mmm.utils.PropUtils.isLowQualityModule`,
it is a check that verify that the module is declaring the minimum required to
allow the app to show your module to the user without hurting his experience.
Filling all basic Magisk properties is often enough to not get filtered out by it.
@ -100,7 +100,7 @@ Note²: For `minMagisk`, `XX.Y` is parsed as `XXY00`, so you can just put the Ma
(For example `- Hello world` will be transformed to `[*] Hello world`, do not apply to modules installed from storage)
Note: Fox's Mmm use fallback
[here](../app/src/main/java/com/fox2code/mmm/utils/io/PropUtils.java#L36)
[here](app/src/main/java/com/fox2code/mmm/utils/PropUtils.java#L36)
for some modules
Theses values are only used if not defined in the `module.prop` files
So the original module maker can still override them
@ -170,7 +170,7 @@ mmm_exec setLastLine "The installer support mmm_exec"
# Wait to simulate the module doing something
sleep 5
mmm_exec hideLoading
mmm_exec setSupportLink https://github.com/Androidacy/MagiskModuleManager
mmm_exec setSupportLink https://github.com/Fox2Code/FoxMagiskModuleManager
```
[You may look at the examples modules and their codes.](examples)

@ -1,110 +1,88 @@
# Androidacy Module Manager
# Fox's Magisk Module Manager
### Developed by Androidacy. Find us on the web [here](https://www.androidacy.com/?utm_source=fox-readme&utm_medium=web&utm_campagin=github).
## Fox Module contest
_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!_
[NoStorageRestrict](https://github.com/Magisk-Modules-Alt-Repo/NoStorageRestrict) by [@DanGLES3](https://github.com/DanGLES3) won via vote in community telegram server.
## About
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.
**This app is not officially supported by Magisk or its developers**
**The modules shown in this app are not affiliated with this app or Magisk**.
## 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
Module description: Removes the restriction when selecting folders (Downloads/Android) through the file manager on Android 11 and higher
## Community
[![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)
[![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)
<a href="https://translate.nift4.org/engage/foxmmm/">
<img src="https://translate.nift4.org/widgets/foxmmm/-/foxmmm/svg-badge.svg" alt="Translation status" />
</a>
### We'd like to thank Fox2Code for his initial work on the app.
## Screenshots
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.
Main activity:
[<img src="screenshot-dark.jpg" width="250"/>](screenshot-dark.jpg)
[<img src="screenshot-light.jpg" width="250"/>](screenshot-light.jpg)
## Screenshots
## What is this?
Main activity:
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.
**This app is not officially supported by Magisk or its developers**
| 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) |
**The modules shown in this app are not affiliated with this app or Magisk**
(Please contact repo owners instead)
## Requirements
### Minimum / Recommended:
Minimum:
- Android 5.0+
- Magisk 19.0+
- An internet connection
- Android 7.0+ / Android 8.0+
- Magisk 19.0+ / Magisk 21.2+
- An internet connection / A stable wifi connection
Recommended:
- Android 6.0+
- Magisk 21.2+
- An internet connection
Note: This app may require the use of a VPN in countries with a state wide firewall.
## Installation
## For users
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.
To install the app go to [releases](https://github.com/Fox2Code/FoxMagiskModuleManager/releases),
and download and install the latest `.apk` 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 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.
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**)
#### [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
#### [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
- 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:**
&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)
Support:
#### [Magisk Alt Repo](https://github.com/Magisk-Modules-Alt-Repo)
[![GitHub issues](https://img.shields.io/github/issues/Magisk-Modules-Alt-Repo/submission)](https://github.com/Magisk-Modules-Alt-Repo/submission/issues)
- 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.
#### [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
- Officially supported by Fox's mmm
- Disabled by default and no longer recommended. Kept as an alternative for those who want it
- Contains ads to help cover server costs
**Support:**
Support:
&emsp; [![GitHub issues](https://img.shields.io/github/issues/Magisk-Modules-Alt-Repo/submission)](https://github.com/Magisk-Modules-Alt-Repo/submission/issues)
[![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)
**Please do not use GitHub issues for help or questions. GitHub issues are specifically for bug
reporting and general app feedback.**
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.
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.
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.
## For developers
The manager can read a few new meta keys to allow modules to customize their own entry
The manager can read 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
@ -112,74 +90,58 @@ 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 allows module developers to craft a more customizable install experience.
It allow module developers to have 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!
We use Weblate for translations: https://translate.nift4.org/engage/foxmmm/
(Make sure to check your spam folder when registering)
**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.
If you do not want to register on the self-hosted Weblate instance, you can do a pull request on GitHub:
### Weblate (recommended)
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)
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)
If your language is right to left don't forget to set `lang_support_rtl` to `true`.
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.**
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!

@ -0,0 +1,238 @@
plugins {
// Gradle doesn't allow conditionally enabling/disabling 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 59
versionName "0.6.7"
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"]
}
// "true" is not allowed inside this block, use "hasSentryConfig" instead.
// This is because gradle doesn't allow to enable/disable plugins conditionally
sentry {
// Disable sentry on F-Droid flavor
ignoredFlavors = hasSentryConfig ? [] : ["default", "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 = hasSentryConfig
// 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 = hasSentryConfig
// 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 = hasSentryConfig
// Enable or disable the tracing instrumentation.
// Does auto instrumentation for specified features through bytecode manipulation.
// Default is enabled.
tracingInstrumentation {
enabled = hasSentryConfig
}
// 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 = hasSentryConfig
// 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.5.0'
}
}
configurations {
implementation.exclude group: 'org.jetbrains', module: 'annotations'
if (!hasSentryConfig) {
implementation.exclude group: 'io.sentry', module: 'sentry-android'
implementation.exclude group: 'io.sentry', module: 'sentry-android-fragment'
implementation.exclude group: 'io.sentry', module: 'sentry-android-okhttp'
implementation.exclude group: 'io.sentry', module: 'sentry-android-core'
implementation.exclude group: 'io.sentry', module: 'sentry-android-ndk'
}
}
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.7.0'
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'
// Update the version code in the root build.gradle
implementation "com.mikepenz:aboutlibraries:${latestAboutLibsRelease}"
// Utils
implementation 'androidx.work:work-runtime:2.7.1'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:5.0.0-alpha.10'
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.10'
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'
if (hasSentryConfig) {
// Error reporting
defaultImplementation 'io.sentry:sentry-android:6.5.0'
defaultImplementation 'io.sentry:sentry-android-fragment:6.5.0'
defaultImplementation 'io.sentry:sentry-android-okhttp:6.5.0'
defaultImplementation 'io.sentry:sentry-android-core:6.5.0'
defaultImplementation 'io.sentry:sentry-android-ndk:6.5.0'
}
// 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.13.2'
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")
}
}
final String sentrySrc = hasSentryConfig ? 'src/sentry/java' : 'src/sentryless/java'
final String sentryManifestSrc = hasSentryConfig ?
'src/sentry/AndroidManifest.xml' : 'src/sentryless/AndroidManifest.xml'
android {
sourceSets {
main {
java.srcDirs += sentrySrc
// manifest.srcFile += sentryManifestSrc // Not supported
}
// Workaround useless gradle restriction
"default" {
manifest.srcFile sentryManifestSrc
}
fdroid {
manifest.srcFile sentryManifestSrc
}
}
}

@ -1,497 +0,0 @@
@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 com.fox2code.mmm.androidacy.AndroidacyWebAPI {
public *;
}
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# 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;
static boolean DEBUG return false;
}
# This is just some proguard rules testes, might do a separate lib after
@ -186,42 +186,3 @@
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

@ -0,0 +1,26 @@
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,31 +1,17 @@
<?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:targetApi="tiramisu">
<uses-sdk tools:overrideLibrary="io.sentry.android" />
tools:ignore="QueryAllPackagesPermission">
<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 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" />
<uses-feature android:name="android.hardware.wifi" android:required="false" />
<!-- Retrieve online modules -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- WebView offline webpage support -->
@ -34,25 +20,17 @@
<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" />
<!-- 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" />
<!-- Supposed to fix bugs with old firmware, only requested on pre Marshmallow -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="22" />
<!-- Post background 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" />
<uses-permission-sdk-23 android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".MainApplication"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:allowBackup="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"
@ -60,31 +38,13 @@
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="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" />
tools:targetApi="s">
<receiver
android:name=".background.BackgroundBootListener"
android:name="com.fox2code.mmm.background.BackgroundBootListener"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
@ -100,19 +60,16 @@
<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:theme="@style/Theme.MagiskModuleManager.NoActionBar" >
android:launchMode="singleTask">
<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"
@ -120,49 +77,24 @@
android:parentActivityName=".MainActivity"
android:screenOrientation="portrait"
tools:ignore="LockedOrientationActivity">
<!--
<intent-filter>
<!-- <intent-filter>
<action android:name="${applicationId}.intent.action.INSTALL_MODULE_INTERNAL" />
</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>
</intent-filter> -->
</activity>
<activity
android:name=".markdown.MarkdownActivity"
android:exported="false"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.MagiskModuleManager.NoActionBar" />
android:theme="@style/Theme.MagiskModuleManager" />
<activity
android:name=".androidacy.AndroidacyActivity"
android:exported="false"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.MagiskModuleManager.NoActionBar" >
<!--
<intent-filter>
android:theme="@style/Theme.MagiskModuleManager">
<!-- <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" />
@ -181,41 +113,14 @@
</provider>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.file-provider"
android:name="androidx.core.content.FileProvider"
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 @@
# shellcheck shell=ash
#!/sbin/sh
#################
# Initialization
@ -20,24 +20,24 @@ require_new_magisk() {
# Load util_functions.sh
#########################
export OUTFD=$2
export ZIPFILE=$3
OUTFD=$2
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,10 +75,9 @@ is_legacy_script() {
print_modname() {
local authlen len namelen pounds
# shellcheck disable=SC2006
namelen=`echo -n "$MODNAME" | wc -c`
authlen=$(($(echo -n "$MODAUTH" | wc -c) + 3))
[ "$namelen" -gt $authlen ] && len=$namelen || len=$authlen
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"
@ -94,7 +93,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
}
@ -102,7 +101,7 @@ abort() {
rm -rf $TMPDIR 2>/dev/null
mkdir -p $TMPDIR
chcon u:object_r:system_file:s0 $TMPDIR || true
cd $TMPDIR || exit
cd $TMPDIR
# Preperation for flashable zips
setup_flashable
@ -129,15 +128,14 @@ 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
@ -154,22 +152,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
@ -220,46 +218,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
@ -267,9 +265,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,11 +1,15 @@
package com.fox2code.mmm;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.net.Http;
import android.util.Log;
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;
@ -14,55 +18,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;
public static final String RELEASES_API_URL = "https://api.github.com/repos/Androidacy/MagiskModuleManager/releases/latest";
private static final String TAG = "AppUpdateManager";
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.latestRelease = MainApplication.getBootSharedPreferences()
.getString("updater_latest_release", BuildConfig.VERSION_NAME);
this.latestPreRelease = MainApplication.getBootSharedPreferences()
.getString("updater_latest_pre_release", BuildConfig.VERSION_NAME);
this.lastChecked = 0;
this.preReleaseNewer = true;
if (this.compatFile.isFile()) {
try {
this.parseCompatibilityFlags(new FileInputStream(this.compatFile));
} catch (
IOException ignored) {
} catch (IOException e) {
e.printStackTrace();
}
}
}
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)
@ -77,99 +81,166 @@ public class AppUpdateManager {
synchronized (this.updateLock) {
if (lastChecked != this.lastChecked)
return this.peekShouldUpdate();
boolean preReleaseNewer = true;
try {
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");
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.
}
}
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;
if (latestRelease != null)
this.latestRelease = latestRelease;
if (latestPreRelease != null) {
this.latestPreRelease = latestPreRelease;
this.preReleaseNewer = preReleaseNewer;
} else if (!preReleaseNewer) {
this.latestPreRelease = "";
this.preReleaseNewer = false;
}
this.latestRelease = latestRelease;
Log.d(TAG, "Latest release: " + latestRelease);
Log.d(TAG, "Latest pre-release: " + latestPreRelease);
Log.d(TAG, "Latest pre-release newer: " + preReleaseNewer);
this.lastChecked = System.currentTimeMillis();
} catch (
Exception ioe) {
Timber.e(ioe);
this.lastCheckSuccess = true;
} catch (Exception ioe) {
this.lastCheckSuccess = false;
Log.e("AppUpdateManager", "Failed to check releases", ioe);
}
}
return this.peekShouldUpdate();
}
public void checkUpdateCompat() {
compatDataId.clear();
if (this.compatFile.exists()) {
long lastUpdate = this.compatFile.lastModified();
if (lastUpdate <= System.currentTimeMillis() &&
lastUpdate + 600_000L > System.currentTimeMillis()) {
return; // Skip update
}
}
try {
Files.write(compatFile, new byte[0]);
} catch (
IOException e) {
Timber.e(e);
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);
}
// 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 || BuildConfig.DEBUG)
if (!BuildConfig.ENABLE_AUTO_UPDATER)
return false;
// 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;
return !(BuildConfig.VERSION_NAME.equals(this.latestRelease) ||
(this.preReleaseNewer &&
BuildConfig.VERSION_NAME.equals(this.latestPreRelease)));
}
public boolean peekHasUpdate() {
if (!BuildConfig.ENABLE_AUTO_UPDATER || BuildConfig.DEBUG)
if (!BuildConfig.ENABLE_AUTO_UPDATER)
return false;
return this.peekShouldUpdate();
return !BuildConfig.VERSION_NAME.equals(this.preReleaseNewer ?
this.latestPreRelease : this.latestRelease);
}
public boolean isLastCheckSuccess() {
return lastCheckSuccess;
}
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 -> {
}
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;
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;
}
}
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,8 +1,6 @@
package com.fox2code.mmm;
@SuppressWarnings("unused")
public enum Constants {
;
public class 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;

@ -1,172 +0,0 @@
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,131 +1,79 @@
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.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.text.SpannableStringBuilder;
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.app.FoxActivity;
import com.fox2code.foxcompat.app.FoxApplication;
import com.fox2code.foxcompat.app.internal.FoxProcessExt;
import com.fox2code.foxcompat.view.FoxThemeWrapper;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.FoxApplication;
import com.fox2code.foxcompat.FoxThemeWrapper;
import com.fox2code.foxcompat.internal.FoxProcessExt;
import com.fox2code.mmm.installer.InstallerInitializer;
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.mmm.sentry.SentryMain;
import com.fox2code.mmm.utils.GMSProviderInstaller;
import com.fox2code.mmm.utils.Http;
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.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.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<>();
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;
@PrismBundle(
includeAll = true,
grammarLocatorClassName = ".Prism4jGrammarLocator"
)
public class MainApplication extends FoxApplication
implements androidx.work.Configuration.Provider {
private static final String TAG = "MainApplication";
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;
@SuppressLint("RestrictedApi")
// Use FoxProcess wrapper helper.
private static final long secret;
@SuppressLint("RestrictedApi") // Use FoxProcess wrapper helper.
private static final boolean wrapped = !FoxProcessExt.isRootLoader();
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 SharedPreferences bootSharedPreferences;
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 | Shell.FLAG_MOUNT_MASTER).setTimeout(15).setInitializers(InstallerInitializer.class));
Random random = new Random();
do {
secret = random.nextLong();
} while (secret == 0);
Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create()
.setFlags(Shell.FLAG_REDIRECT_STDERR)
.setTimeout(10).setInitializers(InstallerInitializer.class)
);
secret = new Random().nextLong();
}
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!");
@ -138,56 +86,16 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
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() {
@ -198,85 +106,88 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
return intent != null && intent.getLongExtra("secret", ~secret) == secret;
}
public static SharedPreferences getSharedPreferences() {
return INSTANCE.getSharedPreferences("mmm", MODE_PRIVATE);
}
public static boolean isShowcaseMode() {
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;
return getSharedPreferences().getBoolean("pref_showcase_mode", false);
}
public static boolean shouldPreventReboot() {
return getSharedPreferences("mmm").getBoolean("pref_prevent_reboot", true);
return getSharedPreferences().getBoolean("pref_prevent_reboot", true);
}
public static boolean isShowIncompatibleModules() {
return getSharedPreferences("mmm").getBoolean("pref_show_incompatible", false);
return getSharedPreferences().getBoolean("pref_show_incompatible", false);
}
public static boolean isForceDarkTerminal() {
return getSharedPreferences("mmm").getBoolean("pref_force_dark_terminal", false);
return getSharedPreferences().getBoolean("pref_force_dark_terminal", false);
}
public static boolean isTextWrapEnabled() {
return getSharedPreferences("mmm").getBoolean("pref_wrap_text", false);
return getSharedPreferences().getBoolean("pref_wrap_text", false);
}
public static boolean isDohEnabled() {
return getSharedPreferences("mmm").getBoolean("pref_dns_over_https", true);
return getSharedPreferences().getBoolean("pref_dns_over_https", true);
}
public static boolean isMonetEnabled() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && getSharedPreferences("mmm").getBoolean("pref_enable_monet", true);
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
getSharedPreferences().getBoolean("pref_enable_monet", true);
}
public static boolean isBlurEnabled() {
return getSharedPreferences("mmm").getBoolean("pref_enable_blur", false);
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
getSharedPreferences().getBoolean("pref_enable_blur", false);
}
public static boolean isDeveloper() {
if (BuildConfig.DEBUG) return true;
return getSharedPreferences("mmm").getBoolean("developer", false);
return BuildConfig.DEBUG ||
getSharedPreferences().getBoolean("developer", false);
}
public static boolean isDisableLowQualityModuleFilter() {
return getSharedPreferences("mmm").getBoolean("pref_disable_low_quality_module_filter", false) && isDeveloper();
return getSharedPreferences().getBoolean("pref_disable_low_quality_module_filter",
false) && isDeveloper();
}
public static boolean isUsingMagiskCommand() {
return InstallerInitializer.peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND && getSharedPreferences("mmm").getBoolean("pref_use_magisk_install_command", false) && isDeveloper();
return InstallerInitializer.peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND
&& getSharedPreferences().getBoolean("pref_use_magisk_install_command", false)
&& isDeveloper();
}
public static boolean isBackgroundUpdateCheckEnabled() {
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);
return !wrapped && getSharedPreferences().getBoolean("pref_background_update_check", true);
}
public static boolean isAndroidacyTestMode() {
return isDeveloper() && getSharedPreferences("mmm").getBoolean("pref_androidacy_test_mode", false);
return isDeveloper() &&
getSharedPreferences().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("mmm").edit().putBoolean("has_root_access", bool).apply();
getSharedPreferences().edit().putBoolean("has_root_access", bool).apply();
}
public static boolean isCrashReportingEnabled() {
return SentryMain.IS_SENTRY_INSTALLED && getSharedPreferences("mmm").getBoolean("pref_crash_reporting", BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING);
return getSharedPreferences().getBoolean("pref_crash_reporting",
BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING && !BuildConfig.DEBUG);
}
public static SharedPreferences getBootSharedPreferences() {
return getSharedPreferences("mmm_boot");
return bootSharedPreferences;
}
public static MainApplication getINSTANCE() {
@ -288,51 +199,92 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
return timeFormat.format(new Date(timeStamp));
}
public static boolean isNotificationPermissionGranted() {
return NotificationManagerCompat.from(INSTANCE).areNotificationsEnabled();
}
@StyleRes
private int managerThemeResId = R.style.Theme_MagiskModuleManager;
private FoxThemeWrapper markwonThemeContext;
private Markwon markwon;
public Markwon getMarkwon() {
if (isCrashHandler) return null;
if (this.markwon != null) return this.markwon;
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(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(SyntaxHighlightPlugin.create(
new Prism4j(new Prism4jGrammarLocator()), new Prism4jSwitchTheme()))
.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("mmm").getString("pref_theme", "system")) {
switch (theme = getSharedPreferences().getString("pref_theme", "system")) {
default:
Timber.w("Unknown theme id: %s", theme);
Log.w("MainApplication", "Unknown theme id: " + 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;
break;
case "black":
themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet_Black : R.style.Theme_MagiskModuleManager_Black;
themeResId = monet ?
R.style.Theme_MagiskModuleManager_Monet_Dark :
R.style.Theme_MagiskModuleManager_Dark;
break;
case "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;
themeResId = monet ?
R.style.Theme_MagiskModuleManager_Monet_Light :
R.style.Theme_MagiskModuleManager_Light;
break;
}
this.setManagerThemeResId(themeResId);
@ -343,106 +295,45 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
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() {
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);
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 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();
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");
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
DynamicColors.applyToActivitiesIfAvailable(this,
new DynamicColorsOptions.Builder().setPrecondition(
(activity, theme) -> isMonetEnabled()).build());
}*/
SharedPreferences sharedPreferences = MainApplication.getSharedPreferences();
// We are only one process so it's ok to do this
SharedPreferences bootPrefs = MainApplication.getSharedPreferences("mmm_boot");
SharedPreferences bootPrefs = MainApplication.bootSharedPreferences =
this.getSharedPreferences("mmm_boot", MODE_PRIVATE);
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();
}
@ -453,34 +344,24 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
// 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(() -> {
Timber.i("Loading emoji compat...");
Log.d("MainApplication", "Loading emoji compat...");
emojiCompat.load();
Timber.i("Emoji compat loaded!");
Log.d("MainApplication", "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();
}
}
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());
SentryMain.initialize(this);
}
@Override
@ -497,256 +378,12 @@ public class MainApplication extends FoxApplication implements androidx.work.Con
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
Locale newTimeFormatLocale = newConfig.getLocales().get(0);
Locale newTimeFormatLocale = newConfig.locale;
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,5 +1,6 @@
package com.fox2code.mmm;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
@ -7,36 +8,26 @@ import androidx.annotation.AttrRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.foxcompat.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,
androidx.appcompat.R.attr.colorPrimary, com.google.android.material.R.attr.colorOnPrimary) {
R.attr.colorPrimary, R.attr.colorOnPrimary) {
@Override
public boolean shouldRemove() {
return !MainApplication.isShowcaseMode();
@ -62,7 +53,9 @@ 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 ||
@ -73,13 +66,8 @@ public enum NotificationType implements NotificationTypeCst {
NO_INTERNET(R.string.fail_internet, R.drawable.ic_baseline_cloud_off_24) {
@Override
public boolean shouldRemove() {
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();
return AppUpdateManager.getAppUpdateManager().isLastCheckSuccess() ||
RepoManager.getINSTANCE().hasConnectivity();
}
},
NEED_CAPTCHA_ANDROIDACY(R.string.androidacy_need_captcha, R.drawable.ic_baseline_refresh_24, v ->
@ -98,15 +86,17 @@ public enum NotificationType implements NotificationTypeCst {
}
},
UPDATE_AVAILABLE(R.string.app_update_available, R.drawable.ic_baseline_system_update_24,
androidx.appcompat.R.attr.colorPrimary, com.google.android.material.R.attr.colorOnPrimary, v -> IntentHelper.openUrl(v.getContext(),
"https://github.com/Androidacy/MagiskModuleManager/releases"), false) {
R.attr.colorPrimary, R.attr.colorOnPrimary, v -> {
IntentHelper.openUrl(v.getContext(),
"https://github.com/Fox2Code/FoxMagiskModuleManager/releases");
}, false) {
@Override
public boolean shouldRemove() {
return !AppUpdateManager.getAppUpdateManager().peekShouldUpdate();
}
},
INSTALL_FROM_STORAGE(R.string.install_from_storage, R.drawable.ic_baseline_storage_24,
androidx.appcompat.R.attr.colorBackgroundFloating, com.google.android.material.R.attr.colorOnBackground, v -> {
R.attr.colorBackgroundFloating, R.attr.colorOnBackground, v -> {
FoxActivity compatActivity = FoxActivity.getFoxActivity(v);
final File module = new File(compatActivity.getCacheDir(),
"installer" + File.separator + "module.zip");
@ -119,7 +109,7 @@ public enum NotificationType implements NotificationTypeCst {
}
if (needPatch(d)) {
if (d.exists() && !d.delete())
Timber.w("Failed to delete non module zip");
Log.w(TAG, "Failed to delete non module zip");
Toast.makeText(compatActivity,
R.string.invalid_format, Toast.LENGTH_SHORT).show();
} else {
@ -131,7 +121,7 @@ public enum NotificationType implements NotificationTypeCst {
}
} catch (IOException ignored) {
if (d.exists() && !d.delete())
Timber.w("Failed to delete invalid module");
Log.w(TAG, "Failed to delete invalid module");
Toast.makeText(compatActivity,
R.string.invalid_format, Toast.LENGTH_SHORT).show();
}
@ -152,37 +142,12 @@ public enum NotificationType implements NotificationTypeCst {
}
};
public static boolean needPatch(File target) {
private static boolean needPatch(File target) throws IOException {
try (ZipFile zipFile = new ZipFile(target)) {
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 zipFile.getEntry("module.prop") == null &&
zipFile.getEntry("anykernel.sh") == null &&
zipFile.getEntry("META-INF/com/google/android/magisk/module.prop") == null;
}
return false;
}
@StringRes
@ -197,11 +162,11 @@ public enum NotificationType implements NotificationTypeCst {
public final boolean special;
NotificationType(@StringRes int textId, int iconId) {
this(textId, iconId, androidx.appcompat.R.attr.colorError, com.google.android.material.R.attr.colorOnPrimary);
this(textId, iconId, R.attr.colorError, R.attr.colorOnPrimary);
}
NotificationType(@StringRes int textId, int iconId, View.OnClickListener onClickListener) {
this(textId, iconId, androidx.appcompat.R.attr.colorError, com.google.android.material.R.attr.colorOnPrimary, onClickListener);
this(textId, iconId, R.attr.colorError, R.attr.colorOnPrimary, onClickListener);
}
NotificationType(@StringRes int textId, int iconId, int backgroundAttr, int foregroundAttr) {
@ -214,7 +179,7 @@ public enum NotificationType implements NotificationTypeCst {
}
NotificationType(@StringRes int textId, int iconId, int backgroundAttr, int foregroundAttr,
View.OnClickListener onClickListener, @SuppressWarnings("SameParameterValue") boolean special) {
View.OnClickListener onClickListener, boolean special) {
this.textId = textId;
this.iconId = iconId;
this.backgroundAttr = backgroundAttr;
@ -224,7 +189,6 @@ public enum NotificationType implements NotificationTypeCst {
}
public boolean shouldRemove() {
// By default, remove the notification`
return false;
}

@ -1,396 +0,0 @@
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);
}
}
}
}

@ -1,372 +0,0 @@
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,11 +16,8 @@ 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 enum XHooks {
;
public class XHooks {
@Keep
public static void onRepoManagerInitialize() {
// Call addXRepo here if you are an XPosed module

@ -5,12 +5,11 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.net.http.SslError;
import android.os.Build;
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;
@ -23,38 +22,35 @@ 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.app.FoxActivity;
import com.fox2code.foxcompat.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.matomo.sdk.extra.TrackHelper;
import org.json.JSONException;
import org.json.JSONObject;
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) {
@ -72,31 +68,29 @@ public final class AndroidacyActivity extends FoxActivity {
@SuppressWarnings("deprecation")
@Override
@SuppressLint({"SetJavaScriptEnabled", "JavascriptInterface", "RestrictedApi", "ClickableViewAccessibility"})
@SuppressLint({"SetJavaScriptEnabled", "JavascriptInterface", "RestrictedApi"})
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) {
Timber.w("Impersonation detected");
if (!MainApplication.checkSecret(intent) ||
(uri = intent.getData()) == null) {
Log.w(TAG, "Impersonation detected");
this.forceBackPressed();
return;
}
String url = uri.toString();
if (!AndroidacyUtil.isAndroidacyLink(url, uri)) {
Timber.w("Calling non androidacy link in secure WebView: %s", url);
Log.w(TAG, "Calling non androidacy link in secure WebView: " + url);
this.forceBackPressed();
return;
}
if (!Http.hasWebView()) {
Timber.w("No WebView found to load url: %s", url);
Log.w(TAG, "No WebView found to load url: " + 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('?')) {
@ -105,27 +99,8 @@ public final class AndroidacyActivity extends FoxActivity {
url = url + '?' + AndroidacyUtil.REFERRER;
}
}
// 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);
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);
@ -144,10 +119,11 @@ 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) {
}
}
@ -155,56 +131,48 @@ 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);
// Disable cache
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
webSettings.setAllowFileAccess(false);
webSettings.setAllowContentAccess(false);
webSettings.setAllowFileAccessFromFileURLs(false);
webSettings.setAllowUniversalAccessFromFileURLs(false);
webSettings.setMediaPlaybackRequiresUserGesture(false);
// enable webview debugging on debug builds
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true);
// Attempt at fixing CloudFlare captcha.
if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_CONTROL)) {
WebSettingsCompat.setRequestedWithHeaderMode(
webSettings, WebSettingsCompat.REQUESTED_WITH_HEADER_MODE_NO_HEADER);
}
// 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);
// 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);
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
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);
// If api level is < 32, use force dark
WebSettingsCompat.setForceDark(webSettings, MainApplication.getINSTANCE().isLightTheme() ?
WebSettingsCompat.FORCE_DARK_OFF : WebSettingsCompat.FORCE_DARK_ON);
}
}
// 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;
// sanitize url
String url = request.getUrl().toString();
//noinspection UnnecessaryCallToStringValueOf
url = String.valueOf(AndroidacyUtil.hideToken(url));
Timber.i("Exiting WebView %s", url);
Log.i(TAG, "Exiting WebView " + // hideToken in case isAndroidacyLink fail.
AndroidacyUtil.hideToken(request.getUrl().toString()));
IntentHelper.openUri(view.getContext(), request.getUrl().toString());
return true;
}
@ -213,10 +181,13 @@ 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;
}
@ -228,16 +199,21 @@ 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));
}
}
@ -247,89 +223,116 @@ 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) {
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());
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;
}
}
return true;
return super.onConsoleMessage(consoleMessage);
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
if (downloadMode) return;
if (newProgress != 100 && progressIndicator.getVisibility() != View.VISIBLE) {
Timber.i("Progress: %d, showing progress bar", newProgress);
if (newProgress != 100 && // Show progress bar
progressIndicator.getVisibility() != View.VISIBLE)
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 && progressIndicator.getVisibility() != View.INVISIBLE) {
Timber.i("Progress: %d, hiding progress bar", newProgress);
progressIndicator.setIndeterminate(true);
progressIndicator.setVisibility(View.GONE);
}
if (newProgress == 100 && // Hide progress bar
progressIndicator.getVisibility() != View.INVISIBLE)
progressIndicator.setVisibility(View.INVISIBLE);
}
});
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 (this.megaIntercept(webView.getUrl(), downloadUrl)) {
// Block request as Androidacy doesn't allow duplicate requests
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))
return;
} else if (moduleId != null) {
// Download module
Timber.i("megaIntercept failure. Forcing onBackPress");
this.onBackPressed();
}
}
androidacyWebAPI.consumedAction = true;
androidacyWebAPI.downloadMode = false;
}
this.backOnResume = true;
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/"}) {
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/"
}) {
if (downloadUrl.startsWith(prefix)) {
return;
}
@ -342,11 +345,21 @@ 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());
// set layout to view
headers.put("Accept-Language", this.getResources()
.getConfiguration().locale.toLanguageTag());
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();
@ -359,7 +372,14 @@ public final class AndroidacyActivity extends FoxActivity {
}
private String moduleIdOfUrl(String url) {
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
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
int i = url.indexOf('?', prefix.length());
if (i == -1) i = url.length();
if (url.startsWith(prefix)) return url.substring(prefix.length(), i);
@ -380,14 +400,20 @@ 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/downloads/", "https://staging-api.androidacy.com/downloads/"}) { // Make both staging and non staging act the same
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;
}
return false;
}
private boolean isDownloadUrl(String url) {
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
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
if (url.startsWith(prefix)) return true;
}
return false;
@ -395,21 +421,20 @@ public final class AndroidacyActivity extends FoxActivity {
private boolean megaIntercept(String pageUrl, String fileUrl) {
if (pageUrl == null || fileUrl == null) 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;
}
if (this.isFileUrl(fileUrl)) {
Log.d(TAG, "megaIntercept(" +
AndroidacyUtil.hideToken(pageUrl) + ", " +
AndroidacyUtil.hideToken(fileUrl) + ")");
} else return false;
final AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI;
String moduleId = AndroidacyUtil.getModuleId(fileUrl);
String moduleId = this.moduleIdOfUrl(fileUrl);
if (moduleId == null) moduleId = this.moduleIdOfUrl(pageUrl);
if (moduleId == null) {
Timber.i("No module id?");
// Re-open the page
this.webView.loadUrl(pageUrl + "&force_refresh=" + System.currentTimeMillis());
Log.d(TAG, "No module id?");
return false;
}
String checksum = AndroidacyUtil.getChecksumFromURL(fileUrl);
String moduleTitle = AndroidacyUtil.getModuleTitle(fileUrl);
androidacyWebAPI.openNativeModuleDialogRaw(fileUrl, moduleId, moduleTitle, checksum, androidacyWebAPI.canInstall());
androidacyWebAPI.openNativeModuleDialogRaw(fileUrl,
moduleId, "", androidacyWebAPI.canInstall());
return true;
}
@ -421,29 +446,21 @@ 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);
}
@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);
return FileProvider.getUriForFile(this,
this.getPackageName() + ".file-provider",
this.moduleFile);
}
}

@ -1,27 +1,21 @@
package com.fox2code.mmm.androidacy;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
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.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 com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.HttpException;
import com.fox2code.mmm.utils.PropUtils;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import org.json.JSONArray;
import org.json.JSONException;
@ -29,22 +23,16 @@ import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;
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 {
public static String ANDROIDACY_DEVICE_ID = null;
public static String token = MainApplication.getSharedPreferences("androidacy").getString("pref_androidacy_api_token", null);
private static final String TAG = "AndroidacyRepoData";
static {
HttpUrl.Builder OK_HTTP_URL_BUILDER = new HttpUrl.Builder().scheme("https");
@ -53,21 +41,24 @@ public final class AndroidacyRepoData extends RepoData {
OK_HTTP_URL_BUILDER.build();
}
@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;
private String token = this.cachedPreferences.getString("pref_androidacy_api_token", null);
public AndroidacyRepoData(File cacheRoot, boolean testMode) {
super(testMode ? RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT : RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT, cacheRoot);
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???
}
}
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-account/membership-checkout/?level=2&discount_code=FOX2CODE&utm_souce=foxmmm&utm_medium=android-app&utm_campaign=fox-upgrade-promo";
this.defaultDonate = "https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm-medium=app&utm_campaign=fox-inapp";
this.defaultSubmitModule = "https://www.androidacy.com/module-repository-applications/";
this.host = testMode ? "staging-api.androidacy.com" : "production-api.androidacy.com";
this.testMode = testMode;
@ -84,235 +75,104 @@ public final class AndroidacyRepoData extends RepoData {
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 {
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;
Http.doHttpGet("https://" + this.host + "/auth/me?token=" + token, false);
} catch (HttpException e) {
if (e.getErrorCode() == 401) {
Timber.w("Invalid token, resetting...");
Log.w(TAG, "Invalid token, resetting...");
// Remove saved preference
SharedPreferences.Editor editor = MainApplication.getSharedPreferences("androidacy").edit();
editor.remove("pref_androidacy_api_token");
SharedPreferences.Editor editor = this.cachedPreferences.edit();
editor.remove("androidacy_api_token");
editor.apply();
return false;
}
throw e;
} catch (JSONException e) {
// response is not JSON
Timber.w("Invalid token, resetting...");
Timber.w(e);
// Remove saved preference
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
// 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;
}
Http.doHttpGet("https://" + this.host + "/ping", false);
} catch (Exception e) {
Timber.e(e, "Failed to ping server");
Log.e(TAG, "Failed to ping server", e);
// Inform user
/*if (!HttpException.shouldTimeout(e)) {
UiThreadHandler.run(() -> Toast.makeText(MainApplication.getINSTANCE(),
R.string.androidacy_server_down, Toast.LENGTH_SHORT).show());
}*/
return false;
}
String deviceId = generateDeviceId();
long time = System.currentTimeMillis();
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.
if (this.androidacyBlockade > time) return false;
this.androidacyBlockade = time + 30_000L;
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");
if (this.token == null) {
this.token = this.cachedPreferences.getString("pref_androidacy_api_token", null);
if (this.token != null && !this.isValidToken(this.token)) {
this.token = null;
}
} else if (!this.isValidToken(token)) {
Timber.i("Token expired, requesting new one...");
token = null;
} else {
Timber.i("Using validated cached token");
} else if (!this.isValidToken(this.token)) {
this.token = null;
}
} catch (IOException e) {
if (HttpException.shouldTimeout(e)) {
Timber.e(e, "We are being rate limited!");
Log.e(TAG, "We are being rate limited!", e);
this.androidacyBlockade = time + 3_600_000L;
}
return false;
}
if (token == null) {
Timber.i("Token is null, requesting new one...");
try {
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));
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", false), StandardCharsets.UTF_8);
// Parse token
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");
Log.e(TAG, "Failed to parse token", e);
// 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());
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");
Log.e(TAG, "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());
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");
}
// Save token to shared preference
MainApplication.getSharedPreferences().edit().putString("pref_androidacy_api_token", token).apply();
} catch (Exception e) {
if (HttpException.shouldTimeout(e)) {
Timber.e(e, "We are being rate limited!");
Log.e(TAG, "We are being rate limited!", e);
this.androidacyBlockade = time + 3_600_000L;
}
Timber.e(e, "Failed to get a new token");
Log.e(TAG, "Failed to get a new token", e);
return false;
}
}
//noinspection SillyAssignment // who are you calling silly?
this.token = token;
return true;
}
@Override
protected List<RepoModule> populate(JSONObject jsonObject) throws JSONException {
Timber.d("AndroidacyRepoData populate start");
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;
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;
}
}
JSONArray jsonArray = jsonObject.getJSONArray("data");
for (RepoModule repoModule : this.moduleHashMap.values()) {
repoModule.processed = false;
}
@ -321,20 +181,11 @@ public final class AndroidacyRepoData extends RepoData {
long lastLastUpdate = 0;
for (int i = 0; i < len; i++) {
jsonObject = jsonArray.getJSONObject(i);
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());
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;
}
// 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;
}
long lastUpdate = jsonObject.getLong("updated_at") * 1000;
lastLastUpdate = Math.max(lastLastUpdate, lastUpdate);
RepoModule repoModule = this.moduleHashMap.get(moduleId);
if (repoModule == null) {
@ -364,9 +215,6 @@ 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;
@ -394,10 +242,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()) {
@ -433,7 +281,7 @@ public final class AndroidacyRepoData extends RepoData {
@Override
public String getUrl() {
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;
return this.token == null ? this.url : this.url + "?token=" + this.token;
}
private String injectToken(String url) {
@ -441,17 +289,16 @@ public final class AndroidacyRepoData extends RepoData {
if (!AndroidacyUtil.isAndroidacyLink(url)) return url;
if (this.testMode) {
if (url.startsWith("https://production-api.androidacy.com/")) {
Timber.e("Got non test mode url: %s", AndroidacyUtil.hideToken(url));
Log.e(TAG, "Got non test mode url: " + AndroidacyUtil.hideToken(url));
url = "https://staging-api.androidacy.com/" + url.substring(38);
}
} else {
if (url.startsWith("https://staging-api.androidacy.com/")) {
Timber.e("Got test mode url: %s", AndroidacyUtil.hideToken(url));
Log.e(TAG, "Got test mode url: " + AndroidacyUtil.hideToken(url));
url = "https://production-api.androidacy.com/" + url.substring(35);
}
}
String token = "token=" + AndroidacyRepoData.token;
String deviceId = "device_id=" + generateDeviceId();
String token = "token=" + this.token;
if (!url.contains(token)) {
if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
return url + '&' + token;
@ -459,13 +306,6 @@ 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;
}
@ -475,9 +315,13 @@ public final class AndroidacyRepoData extends RepoData {
return this.testMode ? super.getName() + " (Test Mode)" : super.getName();
}
String getToken() {
return this.token;
}
public void setToken(String token) {
if (Http.hasWebView()) {
AndroidacyRepoData.token = token;
this.token = token;
}
}
}

@ -5,12 +5,7 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.fox2code.mmm.BuildConfig;
import java.util.Objects;
public enum AndroidacyUtil {
;
public class AndroidacyUtil {
public static final String REFERRER = "utm_source=FoxMMM&utm_medium=app";
public static boolean isAndroidacyLink(@Nullable Uri uri) {
@ -21,86 +16,38 @@ public enum 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("api.androidacy.com") && Objects.requireNonNull(uri.getHost()).endsWith("api.androidacy.com");
return url.startsWith("https://") &&
(i = url.indexOf("/", 8)) != -1 &&
url.substring(8, i).endsWith(".androidacy.com") &&
uri.getHost().endsWith(".androidacy.com");
}
public static boolean isAndroidacyFileUrl(@Nullable String url) {
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;
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;
}
return false;
}
// Avoid logging token
public static String hideToken(@NonNull String url) {
// 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);
}
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);
}
return null;
}
}

@ -5,7 +5,9 @@ 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;
@ -14,7 +16,7 @@ import androidx.annotation.Keep;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import com.fox2code.foxcompat.view.FoxDisplay;
import com.fox2code.foxcompat.FoxDisplay;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
@ -24,26 +26,25 @@ 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.io.Files;
import com.fox2code.mmm.utils.io.Hashes;
import com.fox2code.mmm.utils.PropUtils;
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;
@ -61,17 +62,12 @@ public class AndroidacyWebAPI {
this.downloadMode = false;
}
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;
}
void openNativeModuleDialogRaw(String moduleUrl, String installTitle,
String checksum, boolean canInstall) {
Log.d(TAG, "ModuleDialog, downloadUrl: " + AndroidacyUtil.hideToken(moduleUrl));
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) {
@ -82,17 +78,19 @@ public class AndroidacyWebAPI {
description = this.activity.getString(R.string.no_desc_found);
}
} else {
// URL Decode installTitle
title = installTitle;
title = PropUtils.makeNameFromId(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);
@ -102,16 +100,17 @@ 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);
// close activity
this.activity.runOnUiThread(this.activity::finishAndRemoveTask);
});
builder.setPositiveButton(hasUpdate ?
R.string.update_module : R.string.install_module, (x, y) -> IntentHelper.openInstaller(this.activity,
fModuleUrl, fTitle, fConfig, fChecksum, fMMTReborn));
}
builder.setOnCancelListener(dialogInterface -> {
if (!this.activity.backOnResume)
@ -121,10 +120,11 @@ public class AndroidacyWebAPI {
this.downloadMode = true;
try {
return this.activity.downloadFileAsync(moduleUrl);
} 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());
} 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());
return null;
}
}, "androidacy_repo");
@ -142,10 +142,8 @@ public class AndroidacyWebAPI {
}
void notifyCompatModeRaw(int value) {
if (this.consumedAction)
return;
if (BuildConfig.DEBUG)
Timber.d("Androidacy Compat mode: %s", value);
if (this.consumedAction) return;
Log.d(TAG, "Androidacy Compat mode: " + value);
this.notifiedCompatMode = value;
if (value < 0) {
value = 0;
@ -158,8 +156,7 @@ 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);
}
@ -167,10 +164,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);
}
/**
@ -178,13 +175,11 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public void openUrl(String url) {
if (this.consumedAction)
return;
if (this.consumedAction) return;
this.consumedAction = true;
this.downloadMode = false;
if (BuildConfig.DEBUG)
Timber.d("Received openUrl request: %s", url);
if (Objects.equals(Uri.parse(url).getScheme(), "https")) {
Log.d(TAG, "Received openUrl request: " + url);
if (Uri.parse(url).getScheme().equals("https")) {
IntentHelper.openUrl(this.activity, url);
}
}
@ -194,13 +189,11 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public void openCustomTab(String url) {
if (this.consumedAction)
return;
if (this.consumedAction) return;
this.consumedAction = true;
this.downloadMode = false;
if (BuildConfig.DEBUG)
Timber.d("Received openCustomTab request: %s", url);
if (Objects.equals(Uri.parse(url).getScheme(), "https")) {
Log.d(TAG, "Received openCustomTab request: " + url);
if (Uri.parse(url).getScheme().equals("https")) {
IntentHelper.openCustomTab(this.activity, url);
}
}
@ -228,7 +221,8 @@ 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();
}
/**
@ -242,31 +236,31 @@ public class AndroidacyWebAPI {
}
this.consumedAction = true;
this.downloadMode = false;
if (BuildConfig.DEBUG)
Timber.d("Received install request: " + moduleUrl + " " + installTitle + " " + checksum);
Log.d(TAG, "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()) {
Timber.w("Androidacy didn't provided a checksum!");
Log.w(TAG, "Androidacy WebView 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, moduleId, installTitle, checksum, true);
this.openNativeModuleDialogRaw(moduleUrl, 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) {
@ -275,7 +269,8 @@ 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);
}
}
@ -284,8 +279,7 @@ 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)) {
@ -294,14 +288,12 @@ public class AndroidacyWebAPI {
}
checksum = Hashes.checkSumFormat(checksum);
if (checksum == null || checksum.isEmpty()) {
Timber.w("Androidacy WebView didn't provided a checksum!");
Log.w(TAG, "Androidacy WebView didn't provided a checksum!");
} else if (!Hashes.checkSumValid(checksum)) {
this.forceQuitRaw("Androidacy didn't provided a valid checksum");
return;
}
// Get moduleTitle from url
String moduleTitle = AndroidacyUtil.getModuleTitle(moduleUrl);
this.openNativeModuleDialogRaw(moduleUrl, moduleId, moduleTitle, checksum, this.canInstall());
this.openNativeModuleDialogRaw(moduleUrl, moduleId, checksum, this.canInstall());
}
/**
@ -344,12 +336,16 @@ 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);
}
});
}
@ -359,8 +355,7 @@ 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();
@ -372,13 +367,13 @@ public class AndroidacyWebAPI {
}
/**
* Return true if the module is an Androidacy module.
* Return true if the module is an Andoridacy 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));
}
/**
@ -387,18 +382,15 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public String getAndroidacyModuleFile(String moduleId, String moduleFile) {
moduleId = moduleId.replaceAll("\\.", "").replaceAll("/", "");
if (moduleFile == null || this.consumedAction || !this.isAndroidacyModule(moduleId))
return "";
moduleFile = moduleFile.replaceAll("\\.", "").replaceAll("/", "");
if (moduleFile == null || this.consumedAction ||
!this.isAndroidacyModule(moduleId)) return "";
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 "";
}
}
@ -409,15 +401,15 @@ public class AndroidacyWebAPI {
*/
@JavascriptInterface
public boolean setAndroidacyModuleMeta(String moduleId, String content) {
moduleId = moduleId.replaceAll("\\.", "").replaceAll("/", "");
if (content == null || this.consumedAction || !this.isAndroidacyModule(moduleId))
return false;
File androidacyMetaFile = new File("/data/adb/modules/" + moduleId + "/.androidacy");
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;
}
}
@ -443,12 +435,13 @@ 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:
* <a href="https://source.android.com/setup/start/build-numbers">right here</a>
* https://source.android.com/setup/start/build-numbers
*/
@JavascriptInterface
public int getAndroidVersionCode() {
@ -479,8 +472,9 @@ public class AndroidacyWebAPI {
public int getAccentColor() {
Resources.Theme theme = this.activity.getTheme();
TypedValue typedValue = new TypedValue();
theme.resolveAttribute(androidx.appcompat.R.attr.colorPrimary, typedValue, true);
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
theme.resolveAttribute(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);
@ -502,8 +496,9 @@ public class AndroidacyWebAPI {
public int getBackgroundColor() {
Resources.Theme theme = this.activity.getTheme();
TypedValue typedValue = new TypedValue();
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) {
theme.resolveAttribute(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);
@ -515,9 +510,11 @@ 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,7 +5,6 @@ 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";
@ -13,15 +12,11 @@ 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) {
new Thread(() -> {
BackgroundUpdateChecker.onMainActivityCreate(context);
BackgroundUpdateChecker.onMainActivityCreate(context);
if (MainApplication.isBackgroundUpdateCheckEnabled()) {
BackgroundUpdateChecker.doCheck(context);
}).start();
}
}
}
}

@ -1,249 +1,123 @@
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.io.PropUtils;
import com.fox2code.mmm.utils.PropUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
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);
}
@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());
@NonNull
@Override
public Result doWork() {
if (!NotificationManagerCompat.from(this.getApplicationContext()).areNotificationsEnabled()
|| !MainApplication.isBackgroundUpdateCheckEnabled()) return Result.success();
synchronized (lock) {
doCheck(this.getApplicationContext());
}
return Result.success();
}
static void doCheck(Context context) {
// 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();
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++;
}
}
// 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);
if (moduleUpdateCount != 0) {
postNotification(context, moduleUpdateCount);
}
}
// 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, 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;
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);
NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, builder.build());
}
public static void onMainActivityCreate(Context context) {
// 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 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());
notificationManagerCompat.cancel(BackgroundUpdateChecker.NOTIFICATION_ID);
// 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());
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());
}
public static void onMainActivityResume(Context context) {
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();
NotificationManagerCompat.from(context).cancel(
BackgroundUpdateChecker.NOTIFICATION_ID);
BackgroundUpdateChecker.easterEggActive = false;
}
}

@ -1,14 +1,12 @@
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.os.PowerManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
@ -19,71 +17,62 @@ import androidx.recyclerview.widget.RecyclerView;
import com.fox2code.androidansi.AnsiConstants;
import com.fox2code.androidansi.AnsiParser;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.foxcompat.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.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.fox2code.mmm.utils.PropUtils;
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;
@SuppressWarnings("IOStreamConstructor")
public class InstallerActivity extends FoxActivity {
private static final HashSet<String> extracted = new HashSet<>();
private static final String TAG = "InstallerActivity";
public LinearProgressIndicator progressIndicator;
public BottomNavigationItemView rebootFloatingButton;
public BottomNavigationItemView cancelFloatingButton;
public ExtendedFloatingActionButton rebootFloatingButton;
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())
Timber.e("Failed to mkdir module cache dir!");
Log.e(TAG, "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 -> {
@ -91,7 +80,7 @@ public class InstallerActivity extends FoxActivity {
return false;
});
final Intent intent = this.getIntent();
String target;
final String target;
final String name;
final String checksum;
final boolean noExtensions;
@ -100,16 +89,11 @@ 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)) {
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://")) {
Log.e(TAG, "Security check failed!");
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
@ -123,6 +107,7 @@ 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();
@ -137,10 +122,12 @@ 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 {
@ -151,37 +138,34 @@ public class InstallerActivity extends FoxActivity {
RecyclerView installTerminal;
this.progressIndicator = findViewById(R.id.progress_bar);
this.rebootFloatingButton = findViewById(R.id.install_terminal_reboot_fab);
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));
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(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.getWindow().setFlags( // Note: Doesn't require WAKELOCK permission
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
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(() -> {
// 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");
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");
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... yeah screw you too jk jk
try {
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;
Log.i(TAG, (urlMode ? "Downloading: " : "Loading: ") + target);
rawModule = urlMode ? Http.doHttpGet(target, (progress, max, done) -> {
if (max <= 0 && this.progressIndicator.isIndeterminate())
return;
this.runOnUiThread(() -> {
this.progressIndicator.setIndeterminate(false);
this.progressIndicator.setMax(max);
@ -193,40 +177,28 @@ public class InstallerActivity extends FoxActivity {
this.progressIndicator.setIndeterminate(true);
});
if (this.canceled) return;
androidacyBlame = urlMode && AndroidacyUtil.isAndroidacyFileUrl(target);
if (checksum != null && !checksum.isEmpty()) {
//noinspection UnnecessaryCallToStringValueOf
Timber.i("Checking for checksum: %s", String.valueOf(checksum));
Log.d(TAG, "Checking for checksum: " + 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";
// 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
try (ZipInputStream zipInputStream = new ZipInputStream(
new ByteArrayInputStream(rawModule))) {
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
String entryName = zipEntry.getName();
// check if the zip entry is a directory
if (entryName.equals("tools/ak3-core.sh")) {
noPatch = true;
isAnyKernel3 = true;
@ -235,28 +207,30 @@ 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/update-binary")) {
} else if (entryName.endsWith(
"/META-INF/com/google/android/magisk/module.prop")) {
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) {
this.setInstallStateFinished(false, "! File is not a valid Magisk module or AnyKernel3 zip", "");
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", "");
return;
}
androidacyBlame = false;
if (noPatch) {
if (urlMode) {
errMessage = "Failed to save module zip";
@ -268,6 +242,7 @@ 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();
@ -280,20 +255,24 @@ public class InstallerActivity extends FoxActivity {
errMessage = "Failed to install module zip";
this.doInstall(moduleCache, noExtensions, rootless);
} catch (IOException e) {
Timber.e(e);
Log.e(TAG, errMessage, e);
if (androidacyBlame) {
errMessage += " (" + e.getLocalizedMessage() + ")";
}
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.
Timber.e(e);
this.setInstallStateFinished(false, "! Module is too large to be loaded on this device", "");
Log.e(TAG, "Module too large", 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;
@ -301,30 +280,43 @@ public class InstallerActivity extends FoxActivity {
this.setOnBackPressedCallback(DISABLE_BACK_BUTTON);
this.setDisplayHomeAsUpEnabled(false);
});
Timber.i("Installing: %s", moduleCache.getName());
InstallerController installerController = new InstallerController(this.progressIndicator, this.installerTerminal, file.getAbsoluteFile(), noExtensions);
Log.i(TAG, "Installing: " + 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)) {
ZipArchiveEntry zipEntry = zipFile.getEntry("customize.sh");
ZipEntry 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) {
Timber.i(e);
Log.d(TAG, "Failed ot extract install script via java code", e);
}
installerMonitor = new InstallerMonitor(installScript);
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);
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);
} else {
String arch32 = "true"; // Do nothing by default
boolean needs32bit = false;
@ -342,9 +334,11 @@ 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) {
ZipArchiveEntry updateBinary = zipFile.getEntry("META-INF/com/google/android/update-binary");
ZipEntry 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")) {
@ -356,42 +350,60 @@ 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;
}
ZipArchiveEntry moduleProp = zipFile.getEntry("module.prop");
ZipEntry 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.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";
}
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;
@ -400,32 +412,47 @@ 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);
@ -435,12 +462,26 @@ 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().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);
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);
}
// Note: Sentry only send this info on crash.
if (MainApplication.isCrashReportingEnabled()) {
@ -451,18 +492,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) {
Timber.w("mmtReborn and magiskCmdLine may not work well together");
Log.w(TAG, "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) {
@ -473,96 +514,8 @@ public class InstallerActivity extends FoxActivity {
message = installerMonitor.doCleanUp();
}
}
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));
}
}
}
});
this.setInstallStateFinished(success, message,
installerController.getSupportLink());
}
public static class InstallerController extends CallbackList<String> {
@ -570,10 +523,13 @@ 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;
@ -585,7 +541,7 @@ public class InstallerActivity extends FoxActivity {
@Override
public void onAddElement(String s) {
if (!this.enabled) return;
Timber.i("MSG: %s", s);
Log.d(TAG, "MSG: " + s);
if ("#!useExt".equals(s.trim()) && !this.noExtension) {
this.useExt = true;
return;
@ -610,10 +566,11 @@ 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()));
}
}
@ -629,13 +586,25 @@ public class InstallerActivity extends FoxActivity {
command = rawCommand;
}
switch (command) {
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" -> {
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":
this.isRecoveryBar = false;
if (!arg.isEmpty()) {
try {
@ -660,24 +629,28 @@ public class InstallerActivity extends FoxActivity {
this.progressIndicator.setIndeterminate(true);
}
this.progressIndicator.setVisibility(View.VISIBLE);
}
case "setLoading" -> {
break;
case "setLoading":
this.isRecoveryBar = false;
try {
this.progressIndicator.setProgressCompat(Short.parseShort(arg), true);
this.progressIndicator.setProgressCompat(
Short.parseShort(arg), true);
} catch (Exception ignored) {
}
}
case "hideLoading" -> {
break;
case "hideLoading":
this.isRecoveryBar = false;
this.progressIndicator.setVisibility(View.GONE);
}
case "setSupportLink" -> {
break;
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;
}
case "disableANSI" -> this.terminal.disableAnsi();
break;
case "disableANSI":
this.terminal.disableAnsi();
break;
}
}
@ -688,7 +661,8 @@ 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"));
}
}
@ -697,6 +671,14 @@ 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;
@ -705,12 +687,14 @@ 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) {
Timber.i("Monitor: %s", s);
Log.d(TAG, "Monitor: " + s);
this.lastCommand = s;
}
@ -728,15 +712,98 @@ 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()) Timber.e("Failed to delete failed update");
if (!moduleUpdate.deleteRecursive())
Log.e(TAG, "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())
Timber.e("Failed to delete failed update");
Log.e(TAG, "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,6 +1,7 @@
package com.fox2code.mmm.installer;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -8,16 +9,15 @@ import androidx.annotation.Nullable;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.NotificationType;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.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,6 +30,7 @@ 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;
@ -59,19 +60,11 @@ 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";
@ -105,10 +98,10 @@ public class InstallerInitializer extends Shell.Initializer {
error = ERROR_NO_PATH;
} catch (NoShellException e) {
error = ERROR_NO_SU;
Timber.w(e);
} catch (Exception e) {
Log.w(TAG, "Device don't have root!", e);
} catch (Throwable e) {
error = ERROR_OTHER;
Timber.e(e);
Log.e(TAG, "Something happened", e);
}
if (forceCheck) {
InstallerInitializer.MAGISK_PATH = MAGISK_PATH;
@ -145,9 +138,9 @@ public class InstallerInitializer extends Shell.Initializer {
return null;
}
MAGISK_PATH = output.size() < 3 ? "" : output.get(2);
Timber.i("Magisk runtime path: %s", MAGISK_PATH);
Log.d(TAG, "Magisk runtime path: " + MAGISK_PATH);
MAGISK_VERSION_CODE = Integer.parseInt(output.get(1));
Timber.i("Magisk version code: %s", MAGISK_VERSION_CODE);
Log.d(TAG, "Magisk version code: " + 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())) {
@ -156,7 +149,7 @@ public class InstallerInitializer extends Shell.Initializer {
if (MAGISK_PATH.length() != 0 && Files.existsSU(new File(MAGISK_PATH))) {
InstallerInitializer.MAGISK_PATH = MAGISK_PATH;
} else {
Timber.e("Failed to get Magisk path (Got " + MAGISK_PATH + ")");
Log.e(TAG, "Failed to get Magisk path (Got " + MAGISK_PATH + ")");
MAGISK_PATH = null;
}
InstallerInitializer.MAGISK_VERSION_CODE = MAGISK_VERSION_CODE;

@ -81,7 +81,6 @@ public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.Te
}
}
@SuppressWarnings("unused")
public void removeLastLine() {
synchronized (lock) {
int size = this.terminal.size();
@ -154,7 +153,6 @@ 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.io.net.Http;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.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 && (this.flags & FLAG_MM_REMOTE_MODULE) == 0) {
if (this.updateJson != null) {
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(1000);
this.updateChangeLog = this.updateChangeLog.substring(0, 1000);
this.updateChangeLog = MarkdownUrlLinker.urlLinkify(this.updateChangeLog);
} catch (Exception e) {
this.updateVersion = null;
@ -56,7 +56,8 @@ public class LocalModuleInfo extends ModuleInfo {
this.updateZipUrl = null;
this.updateChangeLog = "";
this.updateChecksum = null;
Timber.w(e, "Failed update checking for module: %s", this.id);
Log.w("LocalModuleInfo",
"Failed update checking for module: " + this.id, e);
}
}
}

@ -1,7 +1,7 @@
package com.fox2code.mmm.manager;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.PropUtils;
/**
* Representation of the module.prop
@ -22,11 +22,10 @@ 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 String id;
public final String id;
public String name;
public String version;
public long versionCode;
@ -46,8 +45,6 @@ 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;
@ -72,7 +69,6 @@ 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,66 +1,55 @@
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;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import timber.log.Timber;
public final class ModuleManager extends SyncManager {
private static final String TAG = "ModuleManager";
// 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 ModuleManager() {
this.moduleInfos = new HashMap<>();
this.bootPrefs = MainApplication.getBootSharedPreferences();
}
private static final ModuleManager INSTANCE = new ModuleManager();
public static ModuleManager getINSTANCE() {
return INSTANCE;
}
public static boolean isModuleActive(String moduleId) {
ModuleInfo moduleInfo = ModuleManager.getINSTANCE().getModules().get(moduleId);
return moduleInfo != null && (moduleInfo.flags & ModuleInfo.FLAGS_MODULE_ACTIVE) != 0;
private ModuleManager() {
this.moduleInfos = new HashMap<>();
this.bootPrefs = MainApplication.getBootSharedPreferences();
}
protected void scanInternal(@NonNull UpdateListener updateListener) {
// if last_shown_setup is not "v2", then refuse to continue
if (!MainApplication.getSharedPreferences("mmm").getString("last_shown_setup", "").equals("v2")) {
return;
}
NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug();
noodleDebug.push("Initialize scan");
boolean firstScan = this.bootPrefs.getBoolean("mm_first_scan", true);
SharedPreferences.Editor editor = firstScan ? this.bootPrefs.edit() : null;
for (ModuleInfo v : this.moduleInfos.values()) {
@ -76,53 +65,19 @@ 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) {
Timber.e("using fallback instead.");
Log.e(TAG, "Failed to detect modules folder, using fallback instead.");
}
if (BuildConfig.DEBUG) Timber.d("Scan");
StringBuilder modulesList = new StringBuilder();
noodleDebug.replace("Scan");
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);
@ -139,7 +94,8 @@ 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);
@ -147,31 +103,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) {
if (BuildConfig.DEBUG) Timber.d(e);
Log.d(TAG, "Failed to parse metadata!", e);
moduleInfo.flags |= FLAG_MM_INVALID;
}
// append moduleID:moduleName to the list
modulesList.append(moduleInfo.id).append(":").append(moduleInfo.versionCode).append(",");
}
noodleDebug.pop();
}
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");
noodleDebug.replace("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
if (BuildConfig.DEBUG) Timber.d(module);
noodleDebug.replace(module);
LocalModuleInfo moduleInfo = moduleInfos.get(module);
if (moduleInfo == null) {
moduleInfo = new LocalModuleInfo(module);
@ -180,24 +136,28 @@ 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) {
if (BuildConfig.DEBUG) Timber.d(e);
Log.d(TAG, "Failed to parse metadata!", e);
moduleInfo.flags |= FLAG_MM_INVALID;
}
}
noodleDebug.pop();
}
if (BuildConfig.DEBUG) Timber.d("Finalize scan");
noodleDebug.replace("Finalize scan");
this.updatableModuleCount = 0;
Iterator<LocalModuleInfo> moduleInfoIterator = this.moduleInfos.values().iterator();
Iterator<LocalModuleInfo> moduleInfoIterator =
this.moduleInfos.values().iterator();
noodleDebug.push("");
while (moduleInfoIterator.hasNext()) {
LocalModuleInfo moduleInfo = moduleInfoIterator.next();
if (BuildConfig.DEBUG) Timber.d(moduleInfo.id);
noodleDebug.replace(moduleInfo.id);
if ((moduleInfo.flags & FLAG_MM_UNPROCESSED) != 0) {
moduleInfoIterator.remove();
continue; // Don't process fallbacks if unreferenced
}
if (moduleInfo.updateJson != null && (moduleInfo.flags & FLAG_MM_REMOTE_MODULE) == 0) {
if (moduleInfo.updateJson != null) {
this.updatableModuleCount++;
} else {
moduleInfo.updateVersion = null;
@ -206,17 +166,20 @@ 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() {
@ -267,24 +230,34 @@ 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,9 +3,12 @@ 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;
@ -13,31 +16,37 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.foxcompat.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 timber.log.Timber;
import eightbitlab.com.blurview.BlurView;
import eightbitlab.com.blurview.RenderScriptBlur;
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 static final String[] variants = new String[]{
"readme.md", "README.MD", ".github/README.md"
};
private TextView actionBarPadding;
private ColorDrawable actionBarBackground;
private BlurView actionBarBlur;
private TextView header;
private TextView footer;
@ -50,7 +59,8 @@ 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;
@ -69,27 +79,36 @@ 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)) {
Timber.e("Impersonation detected!");
Log.e(TAG, "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, 0);
this.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
if (config != null && !config.isEmpty()) {
String configPkg = IntentHelper.getPackageOfConfig(config);
try {
@ -99,22 +118,21 @@ public class MarkdownActivity extends FoxActivity {
return true;
});
} catch (PackageManager.NameNotFoundException e) {
Timber.w("Config package \"" + configPkg + "\" missing for markdown view");
Log.w(TAG, "Config package \"" +
configPkg + "\" missing for markdown view");
}
}
// 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));
Log.i(TAG, "Url for markdown " + 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()));
@ -129,46 +147,67 @@ public class MarkdownActivity extends FoxActivity {
new Thread(() -> {
try {
Timber.i("Downloading");
Log.d(TAG, "Downloading");
byte[] rawMarkdown = getRawMarkdown(url);
Timber.i("Encoding");
Log.d(TAG, "Encoding");
String markdown = new String(rawMarkdown, StandardCharsets.UTF_8);
Timber.i("Done!");
Log.d(TAG, "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) {
Timber.e(e);
runOnUiThread(() -> Toast.makeText(this, R.string.failed_download, Toast.LENGTH_SHORT).show());
Log.e(TAG, "Failed download", e);
runOnUiThread(() -> {
Toast.makeText(this, R.string.failed_download,
Toast.LENGTH_SHORT).show();
this.onBackPressed();
});
}
}, "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()) {
// 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);
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);
}
}
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();
@ -187,7 +226,8 @@ 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) {
@ -197,7 +237,8 @@ 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) {
@ -207,9 +248,14 @@ 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();
});
}
@ -217,27 +263,46 @@ public class MarkdownActivity extends FoxActivity {
}
private String parseAndroidVersion(int 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;
};
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;
}
}
@Override

@ -1,45 +1,51 @@
package com.fox2code.mmm.markdown;
import java.util.ArrayList;
import android.util.Log;
import com.fox2code.mmm.BuildConfig;
import timber.log.Timber;
import java.util.ArrayList;
public enum MarkdownUrlLinker {
;
public class MarkdownUrlLinker {
private static final String TAG = "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;
Timber.d("Linkify url: %s", url.substring(end));
if (BuildConfig.DEBUG) {
Log.d(TAG, "Linkify url: " + url.substring(index, 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());
Timber.i("Added Markdown link to " + linkifyTasks.size() + " urls");
if (prev.end != url.length()) stringBuilder.append(url, prev.end, url.length());
Log.d(TAG, "Added Markdown link to " + linkifyTasks.size() + " urls");
return stringBuilder.toString();
}

@ -4,6 +4,7 @@ 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;
@ -11,8 +12,8 @@ import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.appcompat.app.AlertDialog;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.foxcompat.view.FoxDisplay;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.FoxDisplay;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.androidacy.AndroidacyUtil;
@ -25,14 +26,8 @@ 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() {
@ -44,66 +39,65 @@ 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;
int icon = moduleHolder.hasUpdate() ?
R.drawable.ic_baseline_update_24 :
R.drawable.ic_baseline_system_update_24;
button.setChipIcon(button.getContext().getDrawable(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;
@ -118,15 +112,21 @@ public enum ActionButtonType {
} else {
builder.setMessage(desc);
}
Timber.i("URL: %s", updateZipUrl);
builder.setNegativeButton(R.string.download_module, (x, y) -> IntentHelper.openCustomTab(button.getContext(), updateZipUrl));
Log.d("Test", "URL: " + 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,34 +137,37 @@ public enum ActionButtonType {
}
}
if (markwon != null) {
TextView messageView = Objects.requireNonNull(alertDialog.getWindow()).findViewById(android.R.id.message);
TextView messageView = 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;
}
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!");
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!");
}
update(button, moduleHolder);
}
@ -174,25 +177,23 @@ 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
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();
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();
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));
@ -203,112 +204,43 @@ 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) {
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);
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;
@ -316,7 +248,8 @@ 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;
@ -333,14 +266,28 @@ 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,6 +2,7 @@ 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;
@ -14,25 +15,23 @@ 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.io.PropUtils;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.PropUtils;
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;
@ -58,8 +57,7 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
this.footerPx = -1;
}
@SuppressWarnings("unused")
public ModuleHolder(int footerPx, boolean header) {
public ModuleHolder(int footerPx,boolean header) {
this.moduleId = "";
this.notificationType = null;
this.separator = null;
@ -72,19 +70,30 @@ 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() {
@ -110,7 +119,8 @@ 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() {
@ -124,25 +134,16 @@ 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)) {
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);
} else if (this.moduleInfo.versionCode < this.moduleInfo.updateVersionCode ||
(this.repoModule != null && this.moduleInfo.versionCode <
this.repoModule.moduleInfo.versionCode)) {
return Type.UPDATABLE;
} else {
return Type.INSTALLED;
@ -152,7 +153,8 @@ 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;
@ -160,28 +162,24 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
}
public boolean shouldRemove() {
// 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()));
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();
@ -194,7 +192,8 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
XHooks.checkConfigTargetExists(context, pkg, config);
buttonTypeList.add(ActionButtonType.CONFIG);
} catch (PackageManager.NameNotFoundException e) {
Timber.w("Config package \"" + pkg + "\" missing for module \"" + this.moduleId + "\"");
Log.w(TAG, "Config package \"" + pkg +
"\" missing for module \"" + this.moduleId + "\"");
}
}
}
@ -209,15 +208,11 @@ 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
@ -228,29 +223,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);
}
@NonNull
@Override
public String toString() {
return "ModuleHolder{" + "moduleId='" + moduleId + '\'' + ", notificationType=" + notificationType + ", separator=" + separator + ", footerPx=" + footerPx + '}';
return compare != 0 ? compare :
selfTypeReal == otherTypeReal ?
selfTypeReal.compare(this, o) :
selfTypeReal.compareTo(otherTypeReal);
}
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);
@ -261,15 +256,18 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
if (cmp != 0) return cmp;
return o1.getMainModuleName().compareTo(o2.getMainModuleName());
}
}, INSTALLED(R.string.installed, true, true) {
// get stacktrace for debugging
},
INSTALLED(R.string.installed, true, true) {
@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);
@ -280,7 +278,8 @@ 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;
@ -296,15 +295,18 @@ 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) {
if (o1 == o2) {
return 0;
} else if (o1 == null) {
return -1;
} else if (o2 == null) {
return 1;
} else {
return o1.moduleId.compareTo(o2.moduleId);
}
return 0;
}
}
@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.view.FoxDisplay;
import com.fox2code.foxcompat.FoxDisplay;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.NotificationType;
import com.fox2code.mmm.R;
@ -30,27 +30,22 @@ 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.materialswitch.MaterialSwitch;
import com.google.android.material.switchmaterial.SwitchMaterial;
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);
}
@ -77,7 +72,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
private final CardView cardView;
private final Chip invalidPropsChip;
private final ImageButton buttonAction;
private final MaterialSwitch switchMaterial;
private final SwitchMaterial switchMaterial;
private final TextView titleText;
private final TextView creditText;
private final TextView descriptionText;
@ -86,9 +81,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);
@ -120,16 +115,14 @@ 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;
@ -143,23 +136,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);
}
@ -195,7 +188,8 @@ 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);
}
@ -207,10 +201,24 @@ 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);
@ -222,11 +230,16 @@ 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.contains("substratum")) {
} else if (moduleHolder.moduleId.equals("substratum")) {
this.updateText.setVisibility(View.VISIBLE);
this.updateText.setText(R.string.substratum_builtin_module);
} else {
@ -235,18 +248,21 @@ 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);
}
}
@ -254,12 +270,14 @@ 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);
@ -270,7 +288,9 @@ 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);
@ -286,17 +306,19 @@ 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);
@ -306,26 +328,31 @@ 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 = androidx.appcompat.R.attr.colorBackgroundFloating;
int foregroundAttr = com.google.android.material.R.attr.colorOnBackground;
int backgroundAttr = R.attr.colorBackgroundFloating;
int foregroundAttr = 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(R.string.low_quality_module_desc).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("Actual description for Low-quality module")
.setCancelable(true)
.setPositiveButton(R.string.ok, (x, y) -> x.dismiss()).show();
});
// Backup restore
// foregroundAttr = R.attr.colorOnError;
@ -341,12 +368,8 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
if (bgColor == Color.WHITE) {
bgColor = 0xFFF8F8F8;
}
// 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);
if (theme.getResources().getBoolean(R.bool.force_transparency)) {
bgColor = ColorUtils.setAlphaComponent(bgColor, 0x80);
}
this.titleText.setTextColor(fgColor);
this.buttonAction.setColorFilter(fgColor);
@ -354,7 +377,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
} else {
Resources.Theme theme = this.titleText.getContext().getTheme();
TypedValue value = new TypedValue();
theme.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, value, true);
theme.resolveAttribute(R.attr.colorOnBackground, value, true);
this.buttonAction.setColorFilter(value.data);
this.titleText.setTextColor(value.data);
this.cardView.setBackground(null);
@ -369,4 +392,8 @@ 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,13 +2,12 @@ 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;
@ -19,14 +18,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<>();
@ -45,31 +44,10 @@ 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) {
Timber.w("addNotification(null) called!");
Log.w(TAG, "addNotification(null) called!");
return;
} else {
Timber.i("addNotification(%s) called", notificationType);
}
synchronized (this.updateLock) {
this.notifications.add(notificationType);
@ -83,10 +61,8 @@ public class ModuleViewListBuilder {
}
ModuleManager moduleManager = ModuleManager.getINSTANCE();
moduleManager.runAfterScan(() -> {
Timber.i("A1: %s", moduleManager.getModules().size());
Log.i(TAG, "A1: " + 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,
@ -99,9 +75,6 @@ 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()) {
@ -109,20 +82,10 @@ public class ModuleViewListBuilder {
}
RepoManager repoManager = RepoManager.getINSTANCE();
repoManager.runAfterUpdate(() -> {
Timber.i("A2: %s", repoManager.getModules().size());
Log.i(TAG, "A2: " + repoManager.getModules().size());
boolean no32bitSupport = Build.SUPPORTED_32_BIT_ABIS.length == 0;
for (RepoModule repoModule : repoManager.getModules().values()) {
// 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;
}
if (!repoModule.repoData.isEnabled()) continue;
ModuleInfo moduleInfo = repoModule.moduleInfo;
if (!showIncompatible && (moduleInfo.minApi > Build.VERSION.SDK_INT ||
(moduleInfo.maxApi != 0 && moduleInfo.maxApi < Build.VERSION.SDK_INT) ||
@ -143,51 +106,12 @@ 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 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) {
public void applyTo(final RecyclerView moduleList,final ModuleViewAdapter moduleViewAdapter) {
if (this.updating) return;
this.updating = true;
ModuleManager.getINSTANCE().afterScan();
@ -195,13 +119,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();
@ -246,14 +170,14 @@ public class ModuleViewListBuilder {
}
}
}
moduleHolders.sort(this.moduleSorter);
Collections.sort(moduleHolders, this.moduleSorter);
// Header is always first
//moduleHolders.add(0, headerFooter[0] =
// new ModuleHolder(this.headerPx / 2, true));
moduleHolders.add(0, headerFooter[0] =
new ModuleHolder(this.headerPx, true));
// Footer is always last
//moduleHolders.add(headerFooter[1] =
// new ModuleHolder(this.footerPx * 2, false));
Timber.i("Got " + moduleHolders.size() + " entries!");
moduleHolders.add(headerFooter[1] =
new ModuleHolder(this.footerPx, false));
Log.i(TAG, "Got " + moduleHolders.size() + " entries!");
// Build end
}
} finally {
@ -293,6 +217,10 @@ 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,
@ -314,6 +242,8 @@ 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);
@ -321,9 +251,59 @@ 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) {
Timber.i("Query " + this.query + " -> " + query);
Log.i(TAG, "Query " + this.query + " -> " + query);
this.query = query == null ? "" :
query.trim().toLowerCase(Locale.ROOT);
}
@ -333,7 +313,7 @@ public class ModuleViewListBuilder {
synchronized (this.queryLock) {
String newQuery = query == null ? "" :
query.trim().toLowerCase(Locale.ROOT);
Timber.i("Query change " + this.query + " -> " + newQuery);
Log.i(TAG, "Query change " + this.query + " -> " + newQuery);
if (this.query.equals(newQuery))
return false;
this.query = newQuery;

@ -1,6 +1,8 @@
package com.fox2code.mmm.repo;
import com.fox2code.mmm.utils.io.net.Http;
import android.content.SharedPreferences;
import com.fox2code.mmm.utils.Http;
import org.json.JSONException;
import org.json.JSONObject;
@ -13,8 +15,8 @@ public final class CustomRepoData extends RepoData {
boolean loadedExternal;
String override;
CustomRepoData(String url, File cacheRoot) {
super(url, cacheRoot);
CustomRepoData(String url, File cacheRoot, SharedPreferences cachedPreferences) {
super(url, cacheRoot, cachedPreferences);
}
@Override
@ -28,32 +30,19 @@ 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,162 +1,79 @@
package com.fox2code.mmm.repo;
import com.fox2code.mmm.MainApplication;
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 android.content.Context;
import android.content.SharedPreferences;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import timber.log.Timber;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.utils.PropUtils;
public class CustomRepoManager {
public static final int MAX_CUSTOM_REPOS = 5;
private static final boolean AUTO_RECOMPILE = true;
public static final int MAX_CUSTOM_REPOS = 5;
private final MainApplication mainApplication;
private final RepoManager repoManager;
private final String[] customRepos;
boolean dirty;
private int customReposCount;
boolean dirty;
@SuppressWarnings("unused")
CustomRepoManager(MainApplication mainApplication, RepoManager repoManager) {
this.mainApplication = mainApplication;
this.repoManager = repoManager;
this.customRepos = new String[MAX_CUSTOM_REPOS];
this.customReposCount = 0;
// refuse to load if setup is not complete
if (MainApplication.getSharedPreferences("mmm").getString("last_shown_setup", "").equals("")) {
return;
}
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();
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;
}
}
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;
}
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();
}
}
private SharedPreferences getSharedPreferences() {
return this.mainApplication.getSharedPreferences(
"mmm_custom_repos", Context.MODE_PRIVATE);
}
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;
// 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.getSharedPreferences().edit()
.putString("repo_" + i, repo).apply();
this.dirty = true;
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 customRepoData = (CustomRepoData)
this.repoManager.addOrGet(repo);
customRepoData.override = "custom_repo_" + i;
customRepoData.updateEnabledState();
realm.close();
return customRepoData;
}
public CustomRepoData getRepo(String id) {
return (CustomRepoData) this.repoManager.get(id);
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 void removeRepo(int index) {
@ -164,18 +81,22 @@ 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;
}
@ -185,9 +106,11 @@ 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() {
@ -199,4 +122,5 @@ public class CustomRepoManager {
if (needUpdate) this.dirty = false;
return needUpdate;
}
}

@ -1,20 +1,18 @@
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.io.Files;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.realm.ModuleListCache;
import com.fox2code.mmm.utils.realm.ReposList;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.PropUtils;
import org.json.JSONArray;
import org.json.JSONException;
@ -22,138 +20,60 @@ 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 {
public final JSONObject supportedProperties = new JSONObject();
private static final String TAG = "RepoData";
private final Object populateLock = new Object();
public String url;
public String id;
public File cacheRoot;
public HashMap<String, RepoModule> moduleHashMap;
public JSONObject metaDataCache;
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 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
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");
}
protected RepoData(String url, File cacheRoot, SharedPreferences cachedPreferences) {
this.url = url;
this.id = RepoManager.internalIdOfUrl(url);
this.cacheRoot = cacheRoot;
// metadata cache is a realm database from ModuleListCache
this.metaDataCache = null;
this.cachedPreferences = cachedPreferences;
this.metaDataCache = new File(cacheRoot, "modules.json");
this.moduleHashMap = new HashMap<>();
this.defaultName = url; // Set url as default name
this.forceHide = AppUpdateManager.shouldForceHide(this.id);
// 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 {
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.enabled = (!this.forceHide) && MainApplication.getSharedPreferences()
.getBoolean("pref_" + this.id + "_enabled", this.isEnabledByDefault());
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();
if (!this.cacheRoot.isDirectory()) {
this.cacheRoot.mkdirs();
} 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();
}
} 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() {
@ -164,10 +84,8 @@ public class RepoData extends XRepo {
List<RepoModule> newModules = new ArrayList<>();
synchronized (this.populateLock) {
String name = jsonObject.getString("name").trim();
// 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;
String nameForModules = name.endsWith(" (Official)") ?
name.substring(0, name.length() - 11) : name;
long lastUpdate = jsonObject.getLong("last_update");
for (RepoModule repoModule : this.moduleHashMap.values()) {
repoModule.processed = false;
@ -177,14 +95,9 @@ public class RepoData extends XRepo {
for (int i = 0; i < len; i++) {
JSONObject module = array.getJSONObject(i);
String moduleId = module.getString("id");
// 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");
}
// 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 moduleLastUpdate = module.getLong("last_update");
String moduleNotesUrl = module.getString("notes_url");
String modulePropsUrl = module.getString("prop_url");
@ -192,17 +105,14 @@ 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);
}
}
@ -213,20 +123,16 @@ 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
@ -234,10 +140,7 @@ public class RepoData extends XRepo {
while (moduleInfoIterator.hasNext()) {
RepoModule repoModule = moduleInfoIterator.next();
if (!repoModule.processed) {
boolean delete = new File(this.cacheRoot, repoModule.id + ".prop").delete();
if (!delete) {
throw new RuntimeException("Failed to delete module metadata");
}
new File(this.cacheRoot, repoModule.id + ".prop").delete();
moduleInfoIterator.remove();
} else {
repoModule.moduleInfo.verify();
@ -259,7 +162,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);
}
@ -268,20 +171,16 @@ 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) {
boolean delete = file.delete();
if (!delete) {
throw new RuntimeException("Failed to delete invalid metadata file");
}
file.delete();
}
} else {
Timber.d("Metadata file not found for %s", repoModule.id);
}
repoModule.moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID;
return false;
@ -289,137 +188,77 @@ public class RepoData extends XRepo {
@Override
public boolean isEnabled() {
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;
}
return this.enabled;
}
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled && !this.forceHide;
// 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();
MainApplication.getSharedPreferences().edit()
.putBoolean("pref_" + this.getPreferenceId() + "_enabled", enabled).apply();
}
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);
// 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;
this.enabled = (!this.forceHide) && MainApplication.getSharedPreferences()
.getBoolean("pref_" + this.getPreferenceId() + "_enabled", this.isEnabledByDefault());
}
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,28 +1,22 @@
package com.fox2code.mmm.repo;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import android.util.Log;
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;
@ -32,60 +26,38 @@ import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import timber.log.Timber;
public final class RepoManager extends SyncManager {
public static final String MAGISK_REPO = "https://raw.githubusercontent.com/Magisk-Modules-Repo/submission/modules/modules.json";
private static final String TAG = "RepoManager";
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 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";
private static final String MAGISK_REPO_MANAGER = "https://magisk-modules-repo.github.io/submission/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";
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";
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) {
@ -121,56 +93,55 @@ public final class RepoManager extends SyncManager {
return INSTANCE;
}
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));
};
}
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;
};
}
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;
/**
* 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();
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;
}
@SuppressWarnings("StatementWithEmptyBody")
private void populateDefaultCache(RepoData repoData) {
// 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()) {
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 (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) {
} else if (repoModule.moduleInfo.versionCode >
registeredRepoModule.moduleInfo.versionCode) {
this.modules.put(repoModule.id, repoModule);
}
} else {
Timber.e("Detected module with invalid metadata: " + repoModule.repoName + "/" + repoModule.id);
Log.e(TAG, "Detected module with invalid metadata: " +
repoModule.repoName + "/" + repoModule.id);
}
}
}
@ -188,18 +159,20 @@ 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 (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;
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)) {
//noinspection ReplaceNullCheck
if (this.androidacyRepoData != null) {
if (ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT.equals(url) ||
ANDROIDACY_MAGISK_REPO_ENDPOINT.equals(url)) {
if (this.androidacyRepoData != null)
return this.androidacyRepoData;
} else {
return this.addAndroidacyRepoData();
}
return this.addAndroidacyRepoData();
} else {
return this.addRepoData(url, fallBackName);
}
@ -208,129 +181,98 @@ public final class RepoManager extends SyncManager {
return repoData;
}
@SuppressWarnings("StatementWithEmptyBody")
@SuppressLint("StringFormatInvalid")
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;
protected void scanInternal(@NonNull UpdateListener updateListener) {
// Refuse to start if first_launch is not false in shared preferences
if (MainActivity.doSetupNowRunning) {
return;
}
NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug();
noodleDebug.push("Downloading indexes");
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;
if (!this.hasConnectivity()) {
updateListener.update(STEP3);
return;
}
noodleDebug.push("");
for (int i = 0; i < repoDatas.length; i++) {
if (BuildConfig.DEBUG) Timber.d("Preparing to fetch: %s", repoDatas[i].getName());
moduleToUpdate += (repoUpdaters[i] = new RepoUpdater(repoDatas[i])).fetchIndex();
noodleDebug.replace(repoDatas[i].getName());
moduleToUpdate += (repoUpdaters[i] =
new RepoUpdater(repoDatas[i])).fetchIndex();
updateListener.update(STEP1 / repoDatas.length * (i + 1));
}
if (BuildConfig.DEBUG) Timber.d("Updating meta-data");
noodleDebug.pop();
noodleDebug.replace("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];
if (BuildConfig.DEBUG) Timber.d("Registering %s", repoData.getName());
for (RepoModule repoModule : repoModules) {
noodleDebug.replace(repoData.getName());
Log.d(TAG, "Registering " + repoData.getName());
noodleDebug.push("");
for (RepoModule repoModule:repoModules) {
noodleDebug.replace(repoModule.id);
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 (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) {
} 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) {
Timber.e(e);
Log.e(TAG, "Failed to get \"" + repoModule.id + "\" metadata", e);
}
updatedModules++;
updateListener.update(STEP1 + (STEP2 / (moduleToUpdate != 0 ? moduleToUpdate : 1) * updatedModules));
updateListener.update(STEP1 + (STEP2 / moduleToUpdate * updatedModules));
}
for (RepoModule repoModule : repoUpdaters[i].toApply()) {
noodleDebug.pop();
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 (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) {
} else if (repoModule.moduleInfo.versionCode >
registeredRepoModule.moduleInfo.versionCode) {
this.modules.put(repoModule.id, repoModule);
}
}
}
}
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();
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)));
}
Timber.i("Got " + this.modules.size() + " modules!");
noodleDebug.pop();
Log.i(TAG, "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()) {
@ -345,13 +287,49 @@ public final class RepoManager extends SyncManager {
}
public boolean hasConnectivity() {
return Http.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;
}
private RepoData addRepoData(String url, String fallBackName) {
String id = internalIdOfUrl(url);
File cacheRoot = new File(this.mainApplication.getDataDir(), "repos/" + id);
RepoData repoData = id.startsWith("repo_") ? new CustomRepoData(url, cacheRoot) : new RepoData(url, cacheRoot);
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);
if (fallBackName != null && !fallBackName.isEmpty()) {
repoData.defaultName = fallBackName;
if (repoData instanceof CustomRepoData) {
@ -361,7 +339,10 @@ public final class RepoManager extends SyncManager {
}
}
switch (url) {
case MAGISK_REPO, MAGISK_REPO_MANAGER -> repoData.defaultWebsite = MAGISK_REPO_HOMEPAGE;
case MAGISK_REPO:
case MAGISK_REPO_MANAGER: {
repoData.defaultWebsite = MAGISK_REPO_HOMEPAGE;
}
}
this.repoData.put(url, repoData);
if (this.initialized) {
@ -371,9 +352,11 @@ public final class RepoManager extends SyncManager {
}
private AndroidacyRepoData addAndroidacyRepoData() {
// cache dir is actually under app data
File cacheRoot = this.mainApplication.getDataDirWithPath("realms/repos/androidacy_repo");
AndroidacyRepoData repoData = new AndroidacyRepoData(cacheRoot, MainApplication.isAndroidacyTestMode());
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());
this.repoData.put(ANDROIDACY_MAGISK_REPO_ENDPOINT, repoData);
this.repoData.put(ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT, repoData);
return repoData;
@ -391,7 +374,12 @@ public final class RepoManager extends SyncManager {
return new LinkedHashSet<>(this.repoData.values());
}
public boolean isLastUpdateSuccess() {
return this.repoLastSuccess;
/**
* 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();
}
}

@ -18,7 +18,6 @@ public class RepoModule {
@StringRes
public int qualityText;
public int qualityValue;
public boolean safe;
public RepoModule(RepoData repoData, String id) {
this.repoData = repoData;
@ -26,24 +25,5 @@ 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,28 +1,21 @@
package com.fox2code.mmm.repo;
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 android.util.Log;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
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;
@ -33,51 +26,12 @@ 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;
@ -86,15 +40,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) {
Timber.e(e);
} catch (Exception e) {
Log.e(TAG, "Failed to get manifest of " + this.repoData.id, e);
this.indexRaw = null;
this.toUpdate = Collections.emptyList();
this.toApply = Collections.emptySet();
@ -111,254 +65,17 @@ public class RepoUpdater {
}
public boolean finish() {
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;
}
final boolean success = this.indexRaw != null;
if (this.indexRaw != null) {
try {
// 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) {
Files.write(this.repoData.metaDataCache, this.indexRaw);
} catch (IOException e) {
e.printStackTrace();
}
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
}
return success.get();
this.toUpdate = null;
this.toApply = null;
return success;
}
}

@ -1,89 +0,0 @@
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);
}
}

@ -0,0 +1,70 @@
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);
}
}
}

@ -1,47 +0,0 @@
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,6 +9,7 @@ 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;
@ -20,41 +21,44 @@ import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.util.List;
import timber.log.Timber;
public final class ExternalHelper {
public static final ExternalHelper INSTANCE = new ExternalHelper();
private static final String TAG = "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.isEmpty()) {
Timber.i("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 == null || resolveInfos.isEmpty()) {
Log.d(TAG, "No external provider installed!");
label = TEST_MODE ? "External" : null;
multi = TEST_MODE;
fallback = null;
} else {
ResolveInfo resolveInfo = resolveInfos.get(0);
Timber.i("Found external provider: %s", resolveInfo.activityInfo.packageName);
fallback = new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
Log.d(TAG, "Found external provider: " + 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);
@ -70,9 +74,8 @@ public final class ExternalHelper {
context.startActivity(intent, param);
}
return true;
} catch (
ActivityNotFoundException e) {
Timber.e(e);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Failed to launch activity", e);
}
if (fallback != null) {
if (multi) {
@ -84,28 +87,27 @@ public final class ExternalHelper {
try {
context.startActivity(intent, param);
return true;
} catch (
ActivityNotFoundException e) {
Timber.e(e);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Failed to launch fallback", 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();
}
});
}

@ -0,0 +1,133 @@
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();
}
}

@ -15,49 +15,46 @@
* 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
* NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* NONINFRINGEMENT. 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.io;
package com.fox2code.mmm.utils;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
import timber.log.Timber;
/**
* Open implementation of ProviderInstaller.installIfNeeded
/** 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 enum GMSProviderInstaller {
;
public class GMSProviderInstaller {
private static final String TAG = "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);
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);
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);
}
}
}

@ -0,0 +1,165 @@
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,36 +1,23 @@
package com.fox2code.mmm.utils.io.net;
package com.fox2code.mmm.utils;
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.util.Log;
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 com.fox2code.mmm.repo.RepoManager;
import java.io.ByteArrayOutputStream;
import java.io.File;
@ -38,7 +25,6 @@ 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;
@ -51,9 +37,9 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLException;
import okhttp3.Cache;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.Dns;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
@ -62,30 +48,31 @@ import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.brotli.BrotliInterceptor;
import okhttp3.dnsoverhttps.DnsOverHttps;
import okhttp3.logging.HttpLoggingInterceptor;
import okio.BufferedSink;
import timber.log.Timber;
public enum Http {
;
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 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!");
Log.e(TAG, "Initialized Http too soon!", error);
System.out.flush();
System.err.flush();
try {
@ -100,42 +87,30 @@ public enum Http {
cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.flush(); // Make sure the instance work
} catch (Exception t) {
} catch (Throwable 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));
Log.e(TAG, "No WebView support!", t);
}
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
hasWebView = cookieManager != null;
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.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")};
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);
@ -143,17 +118,22 @@ public enum Http {
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();
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) {
Timber.e(e, "Failed to init DoH");
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;
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;
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();
@ -161,7 +141,8 @@ public enum Http {
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"))) {
} 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());
@ -169,73 +150,30 @@ public enum Http {
}
if (chain.request().header("Accept-Language") == null) {
request.header("Accept-Language", // Send system language to the server
mainApplication.getResources().getConfiguration().getLocales().get(0).toLanguageTag());
mainApplication.getResources().getConfiguration().locale.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");
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.cookieJar(cookieJar = new CDNCookieJar(cookieManager));
httpclientBuilder.dns(Dns.SYSTEM);
httpClient = followRedirects(httpclientBuilder, true).build();
followRedirects(httpclientBuilder, false).build();
httpClientNoRedirect = followRedirects(httpclientBuilder, false).build();
httpclientBuilder.dns(fallbackDNS);
httpClientDoH = followRedirects(httpclientBuilder, true).build();
followRedirects(httpclientBuilder, false).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();
Timber.i("Initialized Http successfully!");
Log.i(TAG, "Initialized Http successfully!");
doh = MainApplication.isDohEnabled();
}
@ -247,6 +185,10 @@ public enum Http {
return doh ? httpClientDoH : httpClient;
}
public static OkHttpClient getHttpClientNoRedirect() {
return doh ? httpClientNoRedirectDoH : httpClientNoRedirect;
}
public static OkHttpClient getHttpClientWithCache() {
return doh ? httpClientWithCacheDoH : httpClientWithCache;
}
@ -257,10 +199,20 @@ public enum Http {
}
}
private static void checkNeedBlockAndroidacyRequest(String url) throws IOException {
if (!RepoManager.isAndroidacyRepoEnabled()) {
if (AndroidacyUtil.isAndroidacyLink(url)) {
throw new IOException("Androidacy repo is disabled, blocking url: " + url);
}
} else if (needCaptchaAndroidacy() && AndroidacyUtil.isAndroidacyLink(url)) {
throw new HttpException("Androidacy require the user to solve a captcha", 403);
}
}
public static boolean needCaptchaAndroidacy() {
return needCaptchaAndroidacyHost != null;
}
public static String needCaptchaAndroidacyHost() {
return needCaptchaAndroidacyHost;
}
@ -269,64 +221,44 @@ public enum Http {
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");
}
checkNeedBlockAndroidacyRequest(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)) {
Timber.e("Failed to fetch " + url.replaceAll("=[^&]*", "=****") + " with code " + response.code());
if (response.code() != 200 && response.code() != 204 &&
(response.code() != 304 || !allowCache)) {
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);
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) 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()) {
private static Object doHttpPostRaw(String url, String data, boolean allowCache, boolean isRedirect) throws IOException {
checkNeedBlockAndroidacyRequest(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)) {
if (BuildConfig.DEBUG_HTTP)
Timber.e("Failed to fetch " + url + ", code: " + response.code() + ", body: " + response.body().string());
if (response.code() != 200 && response.code() != 204 &&
(response.code() != 304 || !allowCache)) {
checkNeedCaptchaAndroidacy(url, response.code());
throw new HttpException(response.code());
}
@ -340,9 +272,10 @@ public enum Http {
}
public static byte[] doHttpGet(String url, ProgressListener progressListener) throws IOException {
Log.d("Http", "Progress URL: " + url);
checkNeedBlockAndroidacyRequest(url);
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());
}
@ -359,7 +292,7 @@ public enum Http {
final long UPDATE_INTERVAL = 100;
long nextUpdate = System.currentTimeMillis() + UPDATE_INTERVAL;
long currentUpdate;
Timber.i("Target: " + target + " Divider: " + divider);
Log.d("Http", "Target: " + target + " Divider: " + divider);
progressListener.onUpdate(0, (int) (target / divider), false);
while (true) {
int read = inputStream.read(buff);
@ -387,67 +320,159 @@ public enum Http {
return androidacyUA;
}
public static String getMagiskUA() {
return "Magisk/" + InstallerInitializer.peekMagiskVersion();
}
public static void setDoh(boolean doh) {
Timber.i("DoH: " + Http.doh + " -> " + 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);
}
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");
}
/**
* 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;
}
public static void ensureURLHandler(Context context) {
if (!urlFactoryInstalled) {
try {
URL.setURLStreamHandlerFactory(new CronetEngine.Builder(context).build().createURLStreamHandlerFactory());
urlFactoryInstalled = true;
} catch (Error ignored) {
// Ignore
/**
* 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;
}
public interface ProgressListener {
void onUpdate(int downloaded, int total, boolean done);
}
/**
* 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.
* <p>
* 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();
}
}
}
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();
@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);
}
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);
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();
}
}
/**

@ -1,4 +1,4 @@
package com.fox2code.mmm.utils.io.net;
package com.fox2code.mmm.utils;
import androidx.annotation.Keep;
@ -23,10 +23,14 @@ public final class HttpException extends IOException {
}
public boolean shouldTimeout() {
return switch (errorCode) {
case 419, 429, 503 -> true;
default -> false;
};
switch (errorCode) {
case 419:
case 429:
case 503:
return true;
default:
return false;
}
}
public static boolean shouldTimeout(Exception exception) {

@ -3,20 +3,23 @@ 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.app.FoxActivity;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
@ -25,8 +28,6 @@ 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;
@ -37,10 +38,8 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
import timber.log.Timber;
public enum IntentHelper {
;
public class IntentHelper {
private static final String TAG = "IntentHelper";
private static final String EXTRA_TAB_SESSION =
"android.support.customtabs.extra.SESSION";
private static final String EXTRA_TAB_COLOR_SCHEME =
@ -51,14 +50,18 @@ public enum 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 = Intent.FLAG_GRANT_READ_URI_PERMISSION;
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;
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) {
Timber.e(e);
Log.e(TAG, "Failed launch of " + uri, e);
}
} else openUrl(context, uri);
}
@ -68,7 +71,6 @@ public enum 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);
@ -77,14 +79,13 @@ public enum IntentHelper {
}
startActivity(context, myIntent, false);
} catch (ActivityNotFoundException e) {
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();
Toast.makeText(context, "No application can handle this request.\n"
+ " Please install a web-browser", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
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);
@ -93,9 +94,9 @@ public enum IntentHelper {
tabIntent.addCategory(Intent.CATEGORY_BROWSABLE);
startActivityEx(context, tabIntent, viewIntent);
} catch (ActivityNotFoundException e) {
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();
Toast.makeText(context, "No application can handle this request.\n"
+ " Please install a web-browser", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
@ -106,7 +107,7 @@ public enum IntentHelper {
public static void openUrlAndroidacy(Context context, String url, boolean allowInstall,
String title,String config) {
if (!Http.hasWebView()) {
Timber.w("Using custom tab for: %s", url);
Log.w(TAG, "Using custom tab for: " + url);
openCustomTab(context, url);
return;
}
@ -125,6 +126,7 @@ public enum IntentHelper {
} catch (ActivityNotFoundException e) {
Toast.makeText(context, "No application can handle this request."
+ " Please install a web-browser", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
@ -150,10 +152,10 @@ public enum IntentHelper {
"am start -a android.intent.action.MAIN " +
"-c org.lsposed.manager.LAUNCH_MANAGER " +
"com.android.shell/.BugreportWarningActivity")
.to(new CallbackList<>() {
.to(new CallbackList<String>() {
@Override
public void onAddElement(String str) {
Timber.i("LSPosed: %s", str);
Log.d(TAG, "LSPosed: " + str);
}
}).submit();
return;
@ -166,6 +168,7 @@ public enum IntentHelper {
} catch (ActivityNotFoundException e) {
Toast.makeText(context,
"Failed to launch module config activity", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
@ -186,9 +189,15 @@ public enum 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);
@ -214,9 +223,18 @@ public enum 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);
}
@ -320,7 +338,7 @@ public enum IntentHelper {
OnFileReceivedCallback callback) {
File destinationFolder;
if (destination == null || (destinationFolder = destination.getParentFile()) == null ||
(!destinationFolder.mkdirs() && !destinationFolder.isDirectory())) {
(!destinationFolder.isDirectory() && !destinationFolder.mkdirs())) {
callback.onReceived(destination, null, RESPONSE_ERROR);
return;
}
@ -342,7 +360,7 @@ public enum IntentHelper {
callback.onReceived(destination, null, RESPONSE_ERROR);
return;
}
Timber.i("FilePicker returned %s", uri);
Log.d(TAG, "FilePicker returned " + uri);
if ("http".equals(uri.getScheme()) ||
"https".equals(uri.getScheme())) {
callback.onReceived(destination, uri, RESPONSE_URL);
@ -372,10 +390,10 @@ public enum IntentHelper {
}
outputStream = new FileOutputStream(destination);
Files.copy(inputStream, outputStream);
Timber.i("File saved at %s", destination);
Log.d(TAG, "File saved at " + destination);
success = true;
} catch (Exception e) {
Timber.e(e);
Log.e(TAG, "failed copy of " + uri, e);
Toast.makeText(compatActivity,
R.string.file_picker_failure,
Toast.LENGTH_SHORT).show();
@ -383,7 +401,7 @@ public enum IntentHelper {
Files.closeSilently(inputStream);
Files.closeSilently(outputStream);
if (!success && destination.exists() && !destination.delete())
Timber.e("Failed to delete artefact!");
Log.e(TAG, "Failed to delete artefact!");
}
callback.onReceived(destination, uri, success ? RESPONSE_FILE : RESPONSE_ERROR);
});

@ -0,0 +1,171 @@
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;
}
}

@ -1,25 +0,0 @@
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
}
}

@ -1,10 +1,11 @@
package com.fox2code.mmm.utils.io;
package com.fox2code.mmm.utils;
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;
@ -20,16 +21,11 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import timber.log.Timber;
@SuppressWarnings("SpellCheckingInspection")
public enum PropUtils {
;
public class 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"
@ -123,10 +119,6 @@ public enum 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) {
@ -149,8 +141,7 @@ public enum 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)) {
@ -173,8 +164,7 @@ public enum PropUtils {
if (local) {
invalid = true;
break;
}
throw new IOException("Invalid module name!");
} throw new IOException("Invalid module name!");
}
readName = true;
moduleInfo.name = value;
@ -338,7 +328,7 @@ public enum PropUtils {
}
}
public static String readModulePropSimple(InputStream inputStream, String what) {
public static String readModuleId(InputStream inputStream) {
if (inputStream == null) return null;
String moduleId = null;
try (BufferedReader bufferedReader = new BufferedReader(
@ -347,20 +337,16 @@ public enum PropUtils {
while ((line = bufferedReader.readLine()) != null) {
while (line.startsWith("\u0000"))
line = line.substring(1);
if (line.startsWith(what + "=")) {
moduleId = line.substring(what.length() + 1).trim();
if (line.startsWith("id=")) {
moduleId = line.substring(3).trim();
}
}
} catch (IOException e) {
Timber.i(e);
Log.d("PropUtils", "Failed to get moduleId", 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);
@ -386,8 +372,7 @@ public enum 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
|| (moduleInfo.id.startsWith("."));
|| (getFlagsForModule(moduleInfo.id) & FLAG_COMPAT_LOW_QUALITY) != 0;
}
private static boolean isInvalidValue(String name) {

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

@ -1,34 +0,0 @@
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())
}
}
}
}

@ -1,186 +0,0 @@
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();
}
}

@ -1,283 +0,0 @@
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");
}
}
}

@ -1,129 +0,0 @@
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,127 +0,0 @@
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;
}
}

@ -1,313 +0,0 @@
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;
}
}

@ -1,118 +0,0 @@
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,141 +0,0 @@
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;
}
}

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,6 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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>

@ -1,5 +0,0 @@
<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,6 +5,5 @@
android:alpha="0.9"
android:viewportHeight="24"
android:viewportWidth="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,2z" />
<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"/>
</vector>

@ -6,8 +6,8 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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" />
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"/>
<path
android:fillColor="@android:color/white"
android:pathData="M12.5,7H11v6l5.3,3.2 0.8,-1.2 -4.5,-2.7z" />
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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" />
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"/>
</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.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"/>
<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"/>
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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" />
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"/>
</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.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"/>
<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"/>
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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"/>
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"/>
</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.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"/>
<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"/>
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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"/>
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"/>
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorError">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.5,11.9l1.4,-1.4L12,12.6l2.1,-2.1 1.4,1.4L13.4,14l2.1,2.1 -1.4,1.4L12,15.4l-2.1,2.1 -1.4,-1.4L10.6,14l-2.1,-2.1zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
</vector>

@ -1,6 +1,6 @@
<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="M16.2,11.5l1.6,-1.6l-3.8,-3.8l-1.6,1.6L8.4,3.6c-0.8,-0.8 -2.1,-0.8 -2.8,0l-1.9,1.9c-0.8,0.8 -0.8,2.1 0,2.8l4.1,4.1L3,17.3V21h3.8l4.8,-4.8l4.1,4.1c1,1 2.2,0.6 2.8,0l1.9,-1.9c0.8,-0.8 0.8,-2.1 0,-2.8L16.2,11.5zM9.2,11.1L5,6.9l1.9,-1.9c0,0 0,0 0,0l1.3,1.3L7,7.5l1.4,1.4l1.2,-1.2l1.5,1.5L9.2,11.1zM17.1,19l-4.1,-4.1l1.9,-1.9l1.5,1.5l-1.2,1.2l1.4,1.4l1.2,-1.2l1.3,1.3L17.1,19z"/>
<path android:fillColor="@android:color/white" android:pathData="M20.7,7c0.4,-0.4 0.4,-1 0,-1.4l-2.3,-2.3c-0.5,-0.5 -1.1,-0.3 -1.4,0l-1.8,1.8l3.8,3.8L20.7,7z"/>
<path android:fillColor="@android:color/white" android:pathData="M16.24,11.51l1.57,-1.57l-3.75,-3.75l-1.57,1.57L8.35,3.63c-0.78,-0.78 -2.05,-0.78 -2.83,0l-1.9,1.9c-0.78,0.78 -0.78,2.05 0,2.83l4.13,4.13L3,17.25V21h3.75l4.76,-4.76l4.13,4.13c0.95,0.95 2.23,0.6 2.83,0l1.9,-1.9c0.78,-0.78 0.78,-2.05 0,-2.83L16.24,11.51zM9.18,11.07L5.04,6.94l1.89,-1.9c0,0 0,0 0,0l1.27,1.27L7.02,7.5l1.41,1.41l1.19,-1.19l1.45,1.45L9.18,11.07zM17.06,18.96l-4.13,-4.13l1.9,-1.9l1.45,1.45l-1.19,1.19l1.41,1.41l1.19,-1.19l1.27,1.27L17.06,18.96z"/>
<path android:fillColor="@android:color/white" android:pathData="M20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.47,-0.47 -1.12,-0.29 -1.41,0l-1.83,1.83l3.75,3.75L20.71,7.04z"/>
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.3,5.3C17.9,4.7 16.5,4.3 15,4c0,0 -0.1,0 -0.1,0c-0.2,0.3 -0.4,0.8 -0.5,1.1c-1.6,-0.2 -3.2,-0.2 -4.8,0C9.5,4.8 9.3,4.4 9.1,4C9.1,4 9,4 9,4c-1.5,0.3 -2.9,0.7 -4.3,1.3c0,0 0,0 0,0c-2.7,4.1 -3.5,8 -3.1,12c0,0 0,0 0,0.1c1.8,1.3 3.5,2.1 5.2,2.7c0,0 0.1,0 0.1,0c0.4,-0.6 0.8,-1.1 1.1,-1.7c0,0 0,-0.1 0,-0.1c-0.6,-0.2 -1.1,-0.5 -1.6,-0.8c0,0 0,-0.1 0,-0.1c0.1,-0.1 0.2,-0.2 0.3,-0.3c0,0 0.1,0 0.1,0c3.4,1.6 7.2,1.6 10.6,0c0,0 0.1,0 0.1,0c0.1,0.1 0.2,0.2 0.3,0.3c0,0 0,0.1 0,0.1c-0.5,0.3 -1.1,0.6 -1.6,0.8c0,0 -0.1,0.1 0,0.1c0.3,0.6 0.7,1.2 1.1,1.7C17.1,20 17.1,20 17.1,20c1.7,-0.5 3.5,-1.3 5.3,-2.7c0,0 0,0 0,-0.1c0.4,-4.5 -0.7,-8.5 -3.1,-12C19.3,5.3 19.3,5.3 19.3,5.3zM8.5,14.9c-1,0 -1.9,-1 -1.9,-2.1s0.8,-2.1 1.9,-2.1c1.1,0 1.9,1 1.9,2.1C10.4,14 9.6,14.9 8.5,14.9zM15.5,14.9c-1,0 -1.9,-1 -1.9,-2.1s0.8,-2.1 1.9,-2.1c1.1,0 1.9,1 1.9,2.1C17.4,14 16.6,14.9 15.5,14.9z"/>
android:pathData="M19.27,5.33C17.94,4.71 16.5,4.26 15,4c-0.03,0 -0.05,0.01 -0.07,0.03c-0.18,0.33 -0.39,0.76 -0.53,1.09c-1.61,-0.24 -3.22,-0.24 -4.8,0C9.46,4.78 9.25,4.36 9.06,4.03C9.05,4.01 9.02,4 8.99,4c-1.5,0.26 -2.93,0.71 -4.27,1.33c-0.01,0 -0.02,0.01 -0.03,0.02c-2.72,4.07 -3.47,8.03 -3.1,11.95c0,0.02 0.01,0.04 0.03,0.05c1.8,1.32 3.53,2.12 5.24,2.65c0.03,0.01 0.06,0 0.07,-0.02c0.4,-0.55 0.76,-1.13 1.07,-1.74c0.02,-0.04 0,-0.08 -0.04,-0.09c-0.57,-0.22 -1.11,-0.48 -1.64,-0.78c-0.04,-0.02 -0.04,-0.08 -0.01,-0.11c0.11,-0.08 0.22,-0.17 0.33,-0.25c0.02,-0.02 0.05,-0.02 0.07,-0.01c3.44,1.57 7.15,1.57 10.55,0c0.02,-0.01 0.05,-0.01 0.07,0.01c0.11,0.09 0.22,0.17 0.33,0.26c0.04,0.03 0.04,0.09 -0.01,0.11c-0.52,0.31 -1.07,0.56 -1.64,0.78c-0.04,0.01 -0.05,0.06 -0.04,0.09c0.32,0.61 0.68,1.19 1.07,1.74C17.07,20 17.1,20.01 17.13,20c1.72,-0.53 3.45,-1.33 5.25,-2.65c0.02,-0.01 0.03,-0.03 0.03,-0.05c0.44,-4.53 -0.73,-8.46 -3.1,-11.95C19.3,5.34 19.29,5.33 19.27,5.33zM8.52,14.91c-1.03,0 -1.89,-0.95 -1.89,-2.12s0.84,-2.12 1.89,-2.12c1.06,0 1.9,0.96 1.89,2.12C10.41,13.96 9.57,14.91 8.52,14.91zM15.49,14.91c-1.03,0 -1.89,-0.95 -1.89,-2.12s0.84,-2.12 1.89,-2.12c1.06,0 1.9,0.96 1.89,2.12C17.38,13.96 16.55,14.91 15.49,14.91z"/>
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<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,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>

@ -5,6 +5,6 @@
android:viewportWidth="24"
android:width="24dp">
<path android:fillColor="@android:color/white"
android:pathData="M20.5,11H19V7c0,-1.1 -0.9,-2 -2,-2h-4V3.5C13,2.1 11.9,1 10.5,1S8,2.1 8,3.5V5H4c-1.1,0 -2,0.9 -2,2v3.8H3.5c1.5,0 2.7,1.2 2.7,2.7s-1.2,2.7 -2.7,2.7H2V20c0,1.1 0.9,2 2,2h3.8v-1.5c0,-1.5 1.2,-2.7 2.7,-2.7 1.5,0 2.7,1.2 2.7,2.7V22H17c1.1,0 2,-0.9 2,-2v-4h1.5c1.4,0 2.5,-1.1 2.5,-2.5S21.9,11 20.5,11z"
android:pathData="M20.5,11H19V7c0,-1.1 -0.9,-2 -2,-2h-4V3.5C13,2.12 11.88,1 10.5,1S8,2.12 8,3.5V5H4c-1.1,0 -1.99,0.9 -1.99,2v3.8H3.5c1.49,0 2.7,1.21 2.7,2.7s-1.21,2.7 -2.7,2.7H2V20c0,1.1 0.9,2 2,2h3.8v-1.5c0,-1.49 1.21,-2.7 2.7,-2.7 1.49,0 2.7,1.21 2.7,2.7V22H17c1.1,0 2,-0.9 2,-2v-4h1.5c1.38,0 2.5,-1.12 2.5,-2.5S21.88,11 20.5,11z"
/>
</vector>

@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M2.8,2.8L1.4,4.2l2.3,2.3C2.6,8.1 2,10 2,12c0,5.5 4.5,10 10,10c2,0 3.9,-0.6 5.5,-1.7l2.3,2.3l1.4,-1.4L2.8,2.8zM12,20c-4.4,0 -8,-3.6 -8,-8c0,-1.5 0.4,-2.9 1.1,-4.1l10.9,10.9C14.9,19.6 13.5,20 12,20zM7.9,5.1L6.5,3.7C8.1,2.6 10,2 12,2c5.5,0 10,4.5 10,10c0,2 -0.6,3.9 -1.7,5.5l-1.5,-1.5C19.6,14.9 20,13.5 20,12c0,-4.4 -3.6,-8 -8,-8C10.5,4 9.1,4.4 7.9,5.1z"/>
android:pathData="M2.81,2.81L1.39,4.22l2.27,2.27C2.61,8.07 2,9.96 2,12c0,5.52 4.48,10 10,10c2.04,0 3.93,-0.61 5.51,-1.66l2.27,2.27l1.41,-1.41L2.81,2.81zM12,20c-4.41,0 -8,-3.59 -8,-8c0,-1.48 0.41,-2.86 1.12,-4.06l10.94,10.94C14.86,19.59 13.48,20 12,20zM7.94,5.12L6.49,3.66C8.07,2.61 9.96,2 12,2c5.52,0 10,4.48 10,10c0,2.04 -0.61,3.93 -1.66,5.51l-1.46,-1.46C19.59,14.86 20,13.48 20,12c0,-4.41 -3.59,-8 -8,-8C10.52,4 9.14,4.41 7.94,5.12z"/>
</vector>

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

Loading…
Cancel
Save