Initial commit

pull/1/head 0.0.1
Fox2Code 3 years ago
commit 60360fb325

@ -0,0 +1 @@
blank_issues_enabled: false

10
.gitignore vendored

@ -0,0 +1,10 @@
*.iml
.gradle
/local.properties
/.idea/
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

@ -0,0 +1,106 @@
# Fox's Magisk Module Manager (Developer documentation)
Note: This doc assume you already read the
[official Magisk module developer guide](https://topjohnwu.github.io/Magisk/guides.html)
Also note that *Fox's Magisk Module Manager* will be shorten to fox *Fox's Mmm* in this doc
Index:
- [Properties](DEVELOPERS.md#properties)
- [Installer commands](DEVELOPERS.md#installer-commands)
## Properties
In addition to the following magisk properties
```properties
id=<string>
name=<string>
version=<string>
versionCode=<int>
author=<string>
description=<string>
```
This the app manager support these new properties
```properties
# Fox's Mmm supported properties
minApi=<int>
minMagisk=<int>
support=<url>
donate=<url>
config=<package>
```
(Note: All urls must start with `https://`, or else will be ignored)
- `minApi` tell the manager which is the minimum SDK version required for the module
(See: [Codenames, Tags, and Build Numbers](https://source.android.com/setup/start/build-numbers))
- `minMagisk` tell the manager which is the minimum Magisk version required for the module
(Often for magisk `xx.y` the version code is `xxy00`)
- `support` support link to direct users when they need support for you modules
- `donate` donate link to direct users to where they can financially support your project
- `config` package name of the application that configure your module
(Note: Locally installed module don't show the button on the install screen)
Note: Fox's Mmm use fallback
[here](app/src/main/java/com/fox2code/mmm/utils/PropUtils.java)
for some modules
Theses values are only used if not defined in the `module.prop` files
## Installer commands
The Fox's Mmm also allow better control over it's installer interface
Fox's Mmm defined the variable `MMM_EXT_SUPPORT` to expose it's extension support
All the commands start with it `#!`, by default the manager process command as log output
unless `#!useExt` is sent to indicate that the app is ready to use commands
Commands:
- `useExt`: Enable the execution of commands
- `addLine <arg>`: Add line to the terminal, this commands can be useful if
you want to display text that start with `#!` inside the terminal
- `setLastLine <arg>`: Set the last line of text displayed in the terminal
- `clearTerminal`: Clear the terminal of any text, making it empty
- `scrollUp`: Scroll up at the top of the terminal
- `scrollDown`: Scroll down at the bottom of the terminal
- `showLoading`: Show an indeterminate progress bar
(Note: the bar is automatically hidden when the install finish)
- `hideLoading`: Hide the indeterminate progress bar if previously shown
- `setSupportLink <url>`: Set support link when loading finishes
(Note: It override the config button if loaded from repo, it's recommended
to only use this command when the script fail, or don't have any config app)
Note:
The current behavior with unknown command is to ignore them,
I may add or remove commands in the future depending of how they are used
A wrapper script to use theses commands could be
```sh
if [ -n "$MMM_EXT_SUPPORT" ]; then
ui_print "#!useExt"
mmm_exec() {
ui_print "$(echo "#!$@")"
}
else
mmm_exec() { true; }
fi
```
And there is an instance of it in use
```sh
# mmm_exec only take effect if inside the loader
mmm_exec showLoading
ui_print "The installer doesn't support mmm_exec"
mmm_exec setLastLine "The installer support mmm_exec"
sleep 5
mmm_exec hideLoading
mmm_exec setSupportLink https://github.com/Fox2Code/FoxMagiskModuleManager
```
You may look at the [example module](example_module) code or
download the [module zip](example_module.zip) and try it yourself
Have fun with the API making the user install experience a unique experience
Also there is the source of the app icon
[here](https://romannurik.github.io/AndroidAssetStudio/icons-launcher.html#foreground.type=clipart&foreground.clipart=extension&foreground.space.trim=0&foreground.space.pad=0.25&foreColor=rgb(255%2C%20255%2C%20255)&backColor=rgb(255%2C%20152%2C%200)&crop=0&backgroundShape=circle&effects=elevate&name=ic_launcher)
.

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

@ -0,0 +1,34 @@
# Fox's Magisk Module Manager
The official Magisk is dropping support to download online modules...
So I made my own app to do that!
**This app is not officially supported by Magisk or it's developers**
# For users
Related commits:
- [Remove online section in modules fragment](https://github.com/topjohnwu/Magisk/commit/f5c982355a2e3380b2b64af4b0caa8f4f7cf9157)
- [Cleanup unused code](https://github.com/topjohnwu/Magisk/commit/8d59caf635591eb23813d75601039bb138f5716b)
- [Remove DoH](https://github.com/topjohnwu/Magisk/commit/acf25aa4d31ee221354019daa097ccff579b8704)
*(Note: DoH was used to fix modules Downloads by preventing MiTM on DNS queries)*
The app currently use these two repo as their modules sources:
[https://github.com/Magisk-Modules-Alt-Repo](https://github.com/Magisk-Modules-Alt-Repo)
[https://github.com/Magisk-Modules-Repo](https://github.com/Magisk-Modules-Repo)
As the main repo may shutting down due to the main app no longer supporting it,
I recommend submitting your modules [here](https://github.com/Magisk-Modules-Alt-Repo/submission) instead
If a module is in both repo, the manager will just pick the most up to date version of the module
# For developers
The manager add and read new meta keys to modules
It use `module.prop` the `minApi` and `minMagisk` properties to detect compatibility
And use the `support` and `donate` key to detect module related links
It also add new ways to control the installer ui via a new command system
For more information please check the [developer documentation](DEVELOPERS.md)

2
app/.gitignore vendored

@ -0,0 +1,2 @@
/build
/release

@ -0,0 +1,66 @@
plugins {
id 'com.android.application'
id 'com.mikepenz.aboutlibraries.plugin'
}
android {
compileSdk 30
defaultConfig {
applicationId "com.fox2code.mmm"
minSdk 21
targetSdk 30
versionCode 1
versionName "0.0.1"
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
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
aboutLibraries {
additionalLicenses {
LGPL_3_0_only
}
}
dependencies {
// UI
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.google.android.material:material:1.4.0'
implementation "com.mikepenz:aboutlibraries:${latestAboutLibsRelease}"
// Utils
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1'
implementation 'com.github.topjohnwu.libsu:io:3.1.2'
// 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 "com.caverock:androidsvg:1.4"
// Test
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,169 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
-keepattributes SourceFile,LineNumberTable,Signature
-printmapping mapping.txt
# Optimisations
-repackageclasses ""
-overloadaggressively
-allowaccessmodification
# Markdown
-dontwarn org.commonmark.ext.gfm.strikethrough.**
-dontwarn pl.droidsonroids.gif.**
# OkHttp
-dontwarn org.bouncycastle.jsse.**
-dontwarn org.openjsse.**
-dontwarn org.conscrypt.**
# AndroidX
-dontwarn sun.misc.**
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# This is just some proguard rules testes, might do a separate lib after
# Made to help optimise the libraries and not the app directly
-assumenosideeffects class * extends android.content.res.Resources {
android.content.res.AssetManager getAssets();
android.graphics.drawable.Drawable getDrawable(int);
android.graphics.drawable.Drawable getDrawable(int, android.content.res.Resources$Theme);
java.lang.CharSequence getText(int);
java.lang.CharSequence getText(int, java.lang.CharSequence);
java.lang.String getString(int);
java.lang.String getString(int, java.lang.Object[]);
int getIdentifier(java.lang.String, java.lang.String, java.lang.String);
}
-assumenosideeffects class android.content.res.Resources$Theme {
android.graphics.drawable.Drawable getDrawable(int);
android.content.res.Resources getResources();
}
-assumenosideeffects class android.content.res.AssetManager {
java.lang.String[] getLocales();
}
-assumenosideeffects class * extends android.content.Context {
android.graphics.drawable.Drawable getWallpaper();
android.graphics.drawable.Drawable getDrawable(int);
java.lang.CharSequence getText(int);
java.lang.String getString(int);
java.lang.String getString(int, java.lang.Object[]);
android.content.Context getApplicationContext();
android.content.res.AssetManager getAssets();
android.content.res.Resources getResources();
android.content.res.Resources$Theme getTheme();
java.lang.Object getSystemService(java.lang.String);
java.lang.Object getSystemService(java.lang.Class);
java.lang.String getSystemServiceName(java.lang.Class);
android.view.Display getDisplay();
}
-assumenosideeffects class * extends android.content.ContextWrapper {
android.content.Context getBaseContext();
}
-assumenosideeffects class * extends android.view.View {
android.graphics.drawable.Drawable getBackground();
android.graphics.drawable.Drawable getForeground();
android.content.res.Resources getResources();
android.content.Context getContext();
android.view.ViewParent getParent();
android.view.Display getDisplay();
android.view.View findViewById(int);
int getId();
# Component attributes
int getVisibility();
int getX();
int getY();
int getWidth();
int getHeight();
int getBaseline();
int getSystemUiVisibility();
boolean isClickable();
boolean isLongClickable();
boolean isFocusable();
boolean isFocusableInTouchMode();
boolean isFocused();
boolean isDirty();
boolean isDrawingCacheEnabled();
boolean hasFocus();
boolean hasFocusable();
}
-assumenosideeffects class * extends android.view.ViewGroup {
android.view.View getFocusedChild();
android.view.View getChildAt(int);
boolean isChildrenDrawnWithCacheEnabled();
boolean isChildrenDrawingOrderEnabled();
int getChildDrawingOrder(int);
int getChildCount();
}
-assumenosideeffects class * extends android.app.Activity {
android.view.View findViewById(int);
android.content.Intent getIntent();
android.view.Window getWindow();
android.view.WindowManager getWindowManager();
android.view.View getCurrentFocus();
android.content.Intent getParentActivityIntent();
android.app.Activity getParent();
android.content.ComponentName getCallingActivity();
java.lang.String getCallingPackage();
android.app.Application getApplication();
}
-assumenosideeffects class * extends android.view.Window {
android.view.WindowInsetsController getInsetsController();
android.view.WindowManager getWindowManager();
android.view.View findViewById(int);
android.view.View getDecorView();
android.content.Context getContext();
android.view.View getCurrentFocus();
android.view.Window getContainer();
int getFeatures();
}
-assumenosideeffects class * extends android.view.WindowManager {
android.view.WindowMetrics getMaximumWindowMetrics();
android.view.WindowMetrics getCurrentWindowMetrics();
android.view.Display getDefaultDisplay();
}
-assumenosideeffects class * extends android.graphics.drawable.Drawable {
android.graphics.drawable.Drawable getCurrent();
android.graphics.Insets getOpticalInsets();
android.graphics.Rect getDirtyBounds();
android.graphics.Rect getBounds();
boolean isFilterBitmap();
boolean isStateful();
boolean isVisible();
}
-assumenosideeffects class android.view.Display {
android.view.DisplayCutout getCutout();
int getDisplayId();
int getWidth();
int getHeight();
int getFlags();
int getRotation();
}
-assumenosideeffects class android.view.DisplayCutout {
android.graphics.Rect getBoundingRectBottom();
android.graphics.Rect getBoundingRectLeft();
android.graphics.Rect getBoundingRectRight();
android.graphics.Rect getBoundingRectTop();
java.util.List getBoundingRects();
int getSafeInsetBottom();
int getSafeInsetLeft();
int getSafeInsetRight();
int getSafeInsetTop();
android.graphics.Insets getWaterfallInsets();
}

@ -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());
}
}

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.fox2code.mmm"
tools:ignore="QueryAllPackagesPermission">
<!-- Retrieve online modules -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Make sure of the module active state by checking enabled modules on boot -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Open config apps for applications -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<!-- Supposed to fix bugs with old firmware, only requested on pre Marshmallow -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="22" />
<application
android:name=".MainApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:testOnly="false"
android:theme="@style/Theme.MagiskModuleManager">
<receiver android:name="com.fox2code.mmm.manager.ModuleBootReceive"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".settings.SettingsActivity"
android:parentActivityName=".MainActivity"
android:exported="true"
android:label="@string/title_activity_settings" >
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:label="@string/app_name_short">
<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:parentActivityName=".MainActivity"
android:exported="false"
android:launchMode="singleTop">
<intent-filter>
<action android:name="${applicationId}.intent.action.INSTALL_MODULE_INTERNAL" />
</intent-filter>
</activity>
<activity
android:name=".markdown.MarkdownActivity"
android:parentActivityName=".MainActivity"
android:exported="false"
android:theme="@style/Theme.MagiskModuleManager">
</activity>
<activity android:name="com.mikepenz.aboutlibraries.ui.LibsActivity"
tools:node="remove"/>
</application>
</manifest>

@ -0,0 +1,3 @@
package com.fox2code.mmm.manager;
parcelable ModuleInfo;

@ -0,0 +1,200 @@
#!/sbin/sh
#################
# Initialization
#################
umask 022
# echo before loading util_functions
ui_print() { echo "$1"; }
require_new_magisk() {
ui_print "*******************************"
ui_print " Please install Magisk v19.0+! "
ui_print "*******************************"
exit 1
}
#########################
# Load util_functions.sh
#########################
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
if [ $MAGISK_VER_CODE -ge 20400 ]; then
# New Magisk have complete installation logic within util_functions.sh
install_module
exit 0
fi
#################
# Legacy Support
#################
TMPDIR=/dev/tmp
PERSISTDIR=/sbin/.magisk/mirror/persist
is_legacy_script() {
unzip -l "$ZIPFILE" install.sh | grep -q install.sh
return $?
}
print_modname() {
local authlen len namelen pounds
namelen=`echo -n $MODNAME | wc -c`
authlen=$((`echo -n $MODAUTH | wc -c` + 3))
[ $namelen -gt $authlen ] && len=$namelen || len=$authlen
len=$((len + 2))
pounds=$(printf "%${len}s" | tr ' ' '*')
ui_print "$pounds"
ui_print " $MODNAME "
ui_print " by $MODAUTH "
ui_print "$pounds"
ui_print "*******************"
ui_print " Powered by Magisk "
ui_print "*******************"
}
# Override abort as old scripts have some issues
abort() {
ui_print "$1"
$BOOTMODE || recovery_cleanup
[ -n $MODPATH ] && rm -rf $MODPATH
rm -rf $TMPDIR
exit 1
}
rm -rf $TMPDIR 2>/dev/null
mkdir -p $TMPDIR
cd $TMPDIR
# Preperation for flashable zips
setup_flashable
# Mount partitions
mount_partitions
# Detect version and architecture
api_level_arch_detect
# Setup busybox and binaries
$BOOTMODE && boot_actions || recovery_actions
##############
# Preparation
##############
# Extract prop file
unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2
[ ! -f $TMPDIR/module.prop ] && abort "! Unable to extract zip file!"
$BOOTMODE && MODDIRNAME=modules_update || MODDIRNAME=modules
MODULEROOT=$NVBASE/$MODDIRNAME
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
rm -rf $MODPATH 2>/dev/null
mkdir -p $MODPATH
##########
# Install
##########
if is_legacy_script; then
unzip -oj "$ZIPFILE" module.prop install.sh uninstall.sh 'common/*' -d $TMPDIR >&2
# Load install script
. $TMPDIR/install.sh
# Callbacks
print_modname
on_install
# Custom uninstaller
[ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh
# Skip mount
$SKIPMOUNT && touch $MODPATH/skip_mount
# prop file
$PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop
# Module info
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
# service scripts
$LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh
ui_print "- Setting permissions"
set_permissions
else
print_modname
unzip -o "$ZIPFILE" customize.sh -d $MODPATH >&2
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
# Default permissions
set_perm_recursive $MODPATH 0 0 0755 0644
fi
# Load customization script
[ -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
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
fi
# Copy over custom sepolicy rules
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"
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
#############
# Finalizing
#############
cd /
$BOOTMODE || recovery_cleanup
rm -rf $TMPDIR
ui_print "- Done"
exit 0

@ -0,0 +1,160 @@
package com.fox2code.mmm;
import android.app.AlertDialog;
import android.content.Context;
import android.util.Log;
import android.widget.ImageButton;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.IntentHelper;
public enum ActionButtonType {
INFO(R.drawable.ic_baseline_info_24) {
@Override
public void doAction(ImageButton button, ModuleHolder moduleHolder) {
IntentHelper.openMarkdown(button.getContext(),
moduleHolder.repoModule.notesUrl,
moduleHolder.repoModule.moduleInfo.name,
moduleHolder.getMainModuleConfig());
}
@Override
public boolean doActionLong(ImageButton button, ModuleHolder moduleHolder) {
Context context = button.getContext();
Toast.makeText(context, context.getString(R.string.module_id_prefix) +
moduleHolder.moduleId, Toast.LENGTH_SHORT).show();
return true;
}
},
UPDATE_INSTALL() {
@Override
public void update(ImageButton button, ModuleHolder moduleHolder) {
int icon = moduleHolder.hasUpdate() ?
R.drawable.ic_baseline_update_24 : R.drawable.ic_baseline_download_24;
button.setImageResource(icon);
}
@Override
public void doAction(ImageButton button, ModuleHolder moduleHolder) {
RepoModule repoModule = moduleHolder.repoModule;
if (repoModule == null) return;
IntentHelper.openInstaller(button.getContext(), repoModule.zipUrl,
repoModule.moduleInfo.name, repoModule.moduleInfo.config);
}
},
UNINSTALL() {
@Override
public void update(ImageButton button, ModuleHolder moduleHolder) {
int icon = moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING) ?
R.drawable.ic_baseline_delete_outline_24 :
moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_ACTIVE) ?
R.drawable.ic_baseline_delete_24 :
R.drawable.ic_baseline_delete_forever_24;
button.setImageResource(icon);
}
@Override
public void doAction(ImageButton button, ModuleHolder moduleHolder) {
if (!ModuleManager.getINSTANCE().setUninstallState(moduleHolder.moduleInfo,
!moduleHolder.moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING))) {
Log.e("ActionButtonType", "Failed to switch uninstalled state!");
}
update(button, moduleHolder);
}
@Override
public boolean doActionLong(ImageButton button, ModuleHolder moduleHolder) {
if (moduleHolder.moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_ACTIVE)) return false;
new AlertDialog.Builder(button.getContext()).setTitle(R.string.master_delete)
.setPositiveButton(R.string.master_delete_yes, (v, i) -> {
if (!ModuleManager.getINSTANCE().masterClear(moduleHolder.moduleInfo)) {
Toast.makeText(button.getContext(), R.string.master_delete_fail,
Toast.LENGTH_SHORT).show();
} else {
moduleHolder.moduleInfo = null;
CompatActivity.getCompatActivity(button).refreshUI();
}
}).setNegativeButton(R.string.master_delete_no, (v, i) -> {}).create().show();
return true;
}
},
CONFIG(R.drawable.ic_baseline_app_settings_alt_24) {
@Override
public void doAction(ImageButton button, ModuleHolder moduleHolder) {
IntentHelper.openConfig(button.getContext(), moduleHolder.getMainModuleConfig());
}
},
SUPPORT() {
@Override
public void update(ImageButton button, ModuleHolder moduleHolder) {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
button.setImageResource(supportIconForUrl(moduleInfo.support));
}
@Override
public void doAction(ImageButton button, ModuleHolder moduleHolder) {
IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().support);
}
},
DONATE() {
@Override
public void update(ImageButton button, ModuleHolder moduleHolder) {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
int icon = R.drawable.ic_baseline_monetization_on_24;
if (moduleInfo.donate.startsWith("https://www.paypal.me/")) {
icon = R.drawable.ic_baseline_paypal_24;
} else if (moduleInfo.donate.startsWith("https://www.patreon.com/")) {
icon = R.drawable.ic_patreon;
}
button.setImageResource(icon);
}
@Override
public void doAction(ImageButton button, ModuleHolder moduleHolder) {
IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate);
}
};
@DrawableRes
public static int supportIconForUrl(String url) {
int icon = R.drawable.ic_baseline_support_24;
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/")) {
icon = R.drawable.ic_baseline_discord_24;
} else if (url.startsWith("https://github.com/")) {
icon = R.drawable.ic_github;
} else if (url.startsWith("https://forum.xda-developers.com/")) {
icon = R.drawable.ic_xda;
}
return icon;
}
@DrawableRes
private final int iconId;
ActionButtonType() {
this.iconId = 0;
}
ActionButtonType(int iconId) {
this.iconId = iconId;
}
public void update(ImageButton button, ModuleHolder moduleHolder) {
button.setImageResource(this.iconId);
}
public abstract void doAction(ImageButton button, ModuleHolder moduleHolder);
public boolean doActionLong(ImageButton button, ModuleHolder moduleHolder) {
return false;
}
}

@ -0,0 +1,18 @@
package com.fox2code.mmm;
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;
public static final int MAGISK_VER_CODE_MAGISK_ZYGOTE = 23002;
public static final String INTENT_INSTALL_INTERNAL =
BuildConfig.APPLICATION_ID + ".intent.action.INSTALL_MODULE_INTERNAL";
public static final String EXTRA_INSTALL_PATH = "extra_install_path";
public static final String EXTRA_INSTALL_NAME = "extra_install_name";
public static final String EXTRA_INSTALL_CONFIG = "extra_install_config";
public static final String EXTRA_MARKDOWN_URL = "extra_markdown_url";
public static final String EXTRA_MARKDOWN_TITLE = "extra_markdown_title";
public static final String EXTRA_MARKDOWN_CONFIG = "extra_markdown_config";
public static final String EXTRA_FADE_OUT = "extra_fade_out";
public static final String EXTRA_FROM_MANAGER = "extra_from_manager";
}

@ -0,0 +1,250 @@
package com.fox2code.mmm;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.LinearLayout;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.settings.SettingsActivity;
import com.fox2code.mmm.utils.IntentHelper;
import com.google.android.material.progressindicator.LinearProgressIndicator;
public class MainActivity extends CompatActivity implements SwipeRefreshLayout.OnRefreshListener,
SearchView.OnQueryTextListener, SearchView.OnCloseListener {
private static final String TAG = "MainActivity";
private static final int PRECISION = 10000;
public final ModuleViewListBuilder moduleViewListBuilder;
public LinearProgressIndicator progressIndicator;
private ModuleViewAdapter moduleViewAdapter;
private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView moduleList;
private LinearLayout searchContainer;
private CardView searchCard;
private SearchView searchView;
private boolean initMode;
public MainActivity() {
this.moduleViewListBuilder = new ModuleViewListBuilder(this);
this.moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
this.initMode = true;
super.onCreate(savedInstanceState);
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_settings_24, v -> {
IntentHelper.startActivity(this, SettingsActivity.class);
return true;
});
setContentView(R.layout.activity_main);
this.setTitle(R.string.app_name);
this.progressIndicator = findViewById(R.id.progress_bar);
this.swipeRefreshLayout = findViewById(R.id.swipe_refresh);
this.moduleList = findViewById(R.id.module_list);
this.searchContainer = findViewById(R.id.search_container);
this.searchCard = findViewById(R.id.search_card);
this.searchView = findViewById(R.id.search_bar);
this.moduleViewAdapter = new ModuleViewAdapter();
this.moduleList.setAdapter(this.moduleViewAdapter);
this.moduleList.setLayoutManager(new LinearLayoutManager(this));
this.moduleList.setItemViewCacheSize(4); // Default is 2
this.swipeRefreshLayout.setOnRefreshListener(this);
this.moduleList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (newState != RecyclerView.SCROLL_STATE_IDLE)
MainActivity.this.searchView.clearFocus();
}
});
this.searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH |
EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_FORCE_ASCII);
this.searchView.setOnQueryTextListener(this);
this.searchView.setOnCloseListener(this);
this.searchView.setOnQueryTextFocusChangeListener((v, h) -> {
if (!h) {
String query = this.searchView.getQuery().toString();
if (query.isEmpty()) {
this.searchView.setIconified(true);
}
}
this.cardIconifyUpdate();
});
this.searchView.setEnabled(false); // Enabled later
this.cardIconifyUpdate();
InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
@Override
public void onPathReceived(String path) {
Log.i(TAG, "Got magisk path: " + path);
if (InstallerInitializer.peekMagiskVersion() <
Constants.MAGISK_VER_CODE_PATH_SUPPORT)
moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED);
if (!MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
ModuleManager.getINSTANCE().scan();
moduleViewListBuilder.appendInstalledModules();
this.commonNext();
}
@Override
public void onFailure(int error) {
Log.i(TAG, "Failed to get magisk path!");
moduleViewListBuilder.addNotification(NotificationType.NO_ROOT);
this.commonNext();
}
public void commonNext() {
if (MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE);
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
runOnUiThread(() -> {
progressIndicator.setIndeterminate(false);
progressIndicator.setMax(PRECISION);
});
Log.i(TAG, "Scanning for modules!");
RepoManager.getINSTANCE().update(value -> runOnUiThread(() ->
progressIndicator.setProgressCompat((int) (value * PRECISION), true)));
runOnUiThread(() -> {
progressIndicator.setVisibility(View.GONE);
searchView.setEnabled(true);
});
if (!RepoManager.getINSTANCE().hasConnectivity())
moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
moduleViewListBuilder.appendRemoteModules();
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
Log.i(TAG, "Finished app opening state!");
}
}, true);
this.initMode = false;
}
private void cardIconifyUpdate() {
this.moduleViewListBuilder.setFooterPx(this.searchContainer.getHeight());
boolean iconified = this.searchView.isIconified();
int backgroundAttr = iconified ?
R.attr.colorSecondary : R.attr.colorPrimarySurface;
Resources.Theme theme = this.searchCard.getContext().getTheme();
TypedValue value = new TypedValue();
theme.resolveAttribute(backgroundAttr, value, true);
this.searchCard.setCardBackgroundColor(value.data);
this.searchCard.setAlpha(iconified ? 0.70F : 1F);
}
@Override
public void refreshUI() {
super.refreshUI();
if (this.initMode) return;
this.initMode = true;
Log.i(TAG, "Item Before");
this.searchView.setQuery("", false);
this.searchView.clearFocus();
this.searchView.setIconified(true);
this.cardIconifyUpdate();
this.moduleViewListBuilder.setQuery(null);
Log.i(TAG, "Item After");
InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
@Override
public void onPathReceived(String path) {
if (InstallerInitializer.peekMagiskVersion() <
Constants.MAGISK_VER_CODE_PATH_SUPPORT)
moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED);
if (!MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
ModuleManager.getINSTANCE().scan();
moduleViewListBuilder.appendInstalledModules();
this.commonNext();
}
@Override
public void onFailure(int error) {
moduleViewListBuilder.addNotification(NotificationType.NO_ROOT);
this.commonNext();
}
public void commonNext() {
Log.i(TAG, "Common Before");
if (MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE);
if (!RepoManager.getINSTANCE().hasConnectivity())
moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
moduleViewListBuilder.appendRemoteModules();
Log.i(TAG, "Common Before applyTo");
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
Log.i(TAG, "Common After");
}
});
this.initMode = false;
}
@Override
public void onRefresh() {
if (this.initMode || this.progressIndicator == null ||
this.progressIndicator.getVisibility() == View.VISIBLE) {
return; // Do not double scan
}
this.progressIndicator.setVisibility(View.VISIBLE);
this.progressIndicator.setProgressCompat(0, false);
this.moduleViewListBuilder.setFooterPx(this.searchContainer.getHeight());
// this.swipeRefreshLayout.setRefreshing(true); ??
new Thread(() -> {
RepoManager.getINSTANCE().update(value -> runOnUiThread(() ->
this.progressIndicator.setProgressCompat((int) (value * PRECISION), true)));
runOnUiThread(() -> {
this.progressIndicator.setVisibility(View.GONE);
this.swipeRefreshLayout.setRefreshing(false);
});
if (!RepoManager.getINSTANCE().hasConnectivity()) {
this.moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
}
this.moduleViewListBuilder.appendRemoteModules();
this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
},"Repo update thread").start();
}
@Override
public boolean onQueryTextSubmit(final String query) {
this.searchView.clearFocus();
if (this.initMode) return false;
if (this.moduleViewListBuilder.setQueryChange(query)) {
new Thread(() -> {
this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
}, "Query update thread").start();
}
return true;
}
@Override
public boolean onQueryTextChange(String query) {
if (this.initMode) return false;
if (this.moduleViewListBuilder.setQueryChange(query)) {
new Thread(() -> {
this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
}, "Query update thread").start();
}
return false;
}
@Override
public boolean onClose() {
if (this.initMode) return false;
if (this.moduleViewListBuilder.setQueryChange(null)) {
new Thread(() -> {
this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
}, "Query update thread").start();
}
return false;
}
}

@ -0,0 +1,171 @@
package com.fox2code.mmm;
import android.app.Application;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.appcompat.view.ContextThemeWrapper;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.utils.GMSProviderInstaller;
import com.fox2code.mmm.utils.Http;
import com.topjohnwu.superuser.Shell;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
import io.noties.markwon.Markwon;
import io.noties.markwon.html.HtmlPlugin;
import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
public class MainApplication extends Application implements CompatActivity.ApplicationCallbacks {
private static final String timeFormatString = "dd MMM yyyy"; // Example: 13 july 2001
private static Locale timeFormatLocale =
Resources.getSystem().getConfiguration().locale;
private static SimpleDateFormat timeFormat =
new SimpleDateFormat(timeFormatString, timeFormatLocale);
private static final Shell.Builder shellBuilder;
private static final int secret;
private static SharedPreferences bootSharedPreferences;
private static MainApplication INSTANCE;
static {
Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create()
.setFlags(Shell.FLAG_REDIRECT_STDERR)
.setTimeout(10).setInitializers(InstallerInitializer.class)
);
secret = new Random().nextInt();
}
public static Shell build(String... command) {
return shellBuilder.build(command);
}
public static void addSecret(Intent intent) {
intent.putExtra("secret", secret);
}
public static boolean checkSecret(Intent intent) {
return intent.getIntExtra("secret", ~secret) == secret;
}
public static SharedPreferences getSharedPreferences() {
return INSTANCE.getSharedPreferences("mmm", MODE_PRIVATE);
}
public static boolean isShowcaseMode() {
return getSharedPreferences().getBoolean("pref_showcase_mode", false);
}
public static boolean isShowIncompatibleModules() {
return getSharedPreferences().getBoolean("pref_show_incompatible", false);
}
public static boolean hasGottenRootAccess() {
return getSharedPreferences().getBoolean("has_root_access", false);
}
public static void setHasGottenRootAccess(boolean bool) {
getSharedPreferences().edit().putBoolean("has_root_access", bool).apply();
}
public static SharedPreferences getBootSharedPreferences() {
return bootSharedPreferences;
}
public static MainApplication getINSTANCE() {
return INSTANCE;
}
public static String formatTime(long timeStamp) {
// new Date(x) also get the local timestamp for format
return timeFormat.format(new Date(timeStamp));
}
@StyleRes
private int managerThemeResId = R.style.Theme_MagiskModuleManager;
private ContextThemeWrapper markwonThemeContext;
private Markwon markwon;
public Markwon getMarkwon() {
if (this.markwon != null)
return this.markwon;
ContextThemeWrapper contextThemeWrapper = this.markwonThemeContext =
new ContextThemeWrapper(this, this.managerThemeResId);
Markwon markwon = Markwon.builder(contextThemeWrapper).usePlugin(HtmlPlugin.create())
.usePlugin(ImagesPlugin.create().addSchemeHandler(
OkHttpNetworkSchemeHandler.create(Http.getHttpclientWithCache()))).build();
return this.markwon = markwon;
}
public void setManagerThemeResId(@StyleRes int resId) {
this.managerThemeResId = resId;
if (this.markwonThemeContext != null)
this.markwonThemeContext.setTheme(resId);
}
@StyleRes
public int getManagerThemeResId() {
return managerThemeResId;
}
@Override
public void onCreate() {
INSTANCE = this;
super.onCreate();
// We are only one process so it's ok to do this
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) {
bootPrefs.edit().clear().putLong("last_boot", lastBoot).apply();
}
@StyleRes int themeResId;
switch (getSharedPreferences().getString("pref_theme", "system")) {
default:
case "system":
themeResId = R.style.Theme_MagiskModuleManager;
break;
case "dark":
themeResId = R.style.Theme_MagiskModuleManager_Dark;
break;
case "light":
themeResId = R.style.Theme_MagiskModuleManager_Light;
break;
}
this.setManagerThemeResId(themeResId);
// Update SSL Ciphers if update is possible
GMSProviderInstaller.installIfNeeded(this);
}
@Override
public void onCreateCompatActivity(CompatActivity compatActivity) {
compatActivity.setTheme(this.managerThemeResId);
}
@Override
public void onRefreshUI(CompatActivity compatActivity) {
compatActivity.setThemeRecreate(this.managerThemeResId);
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
Locale newTimeFormatLocale = newConfig.locale;
if (timeFormatLocale != newTimeFormatLocale) {
timeFormatLocale = newTimeFormatLocale;
timeFormat = new SimpleDateFormat(
timeFormatString, timeFormatLocale);
}
super.onConfigurationChanged(newConfig);
}
}

@ -0,0 +1,238 @@
package com.fox2code.mmm;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
import androidx.annotation.StringRes;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.IntentHelper;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
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 final int footerPx;
public ModuleInfo moduleInfo;
public RepoModule repoModule;
public ModuleHolder(String moduleId) {
this.moduleId = Objects.requireNonNull(moduleId);
this.notificationType = null;
this.separator = null;
this.footerPx = 0;
}
public ModuleHolder(NotificationType notificationType) {
this.moduleId = "";
this.notificationType = Objects.requireNonNull(notificationType);
this.separator = null;
this.footerPx = 0;
}
public ModuleHolder(Type separator) {
this.moduleId = "";
this.notificationType = null;
this.separator = separator;
this.footerPx = 0;
}
public ModuleHolder(int footerPx) {
this.moduleId = "";
this.notificationType = null;
this.separator = null;
this.footerPx = footerPx;
}
public boolean isModuleHolder() {
return this.notificationType == null && this.separator == null;
}
public ModuleInfo getMainModuleInfo() {
return this.repoModule != null ? this.repoModule.moduleInfo : this.moduleInfo;
}
public String getMainModuleName() {
ModuleInfo moduleInfo = this.getMainModuleInfo();
if (moduleInfo == null || moduleInfo.name == null)
throw new Error("Error for " + this.getType().name() + " id " + this.moduleId);
return moduleInfo.name;
}
public String getMainModuleConfig() {
if (this.moduleInfo == null) return null;
String config = this.moduleInfo.config;
if (config == null && this.repoModule != null) {
config = this.repoModule.moduleInfo.config;
}
return config;
}
public String getUpdateTimeText() {
if (this.repoModule == null) return "";
long timeStamp = this.repoModule.lastUpdated;
return timeStamp <= 0 ? "" :
MainApplication.formatTime(timeStamp);
}
public boolean hasFlag(int flag) {
return this.moduleInfo != null && this.moduleInfo.hasFlag(flag);
}
public Type getType() {
if (this.footerPx != 0) {
return Type.FOOTER;
} else if (this.separator != null) {
return Type.SEPARATOR;
} else if (this.notificationType != null) {
return Type.NOTIFICATION;
} else if (this.moduleInfo == null) {
return Type.INSTALLABLE;
} else if (this.repoModule == null) {
return Type.INSTALLED;
} else if (this.moduleInfo.versionCode <
this.repoModule.moduleInfo.versionCode) {
return Type.UPDATABLE;
} else {
return Type.INSTALLED;
}
}
public Type getCompareType(Type type) {
if (this.separator != null) {
return this.separator;
} else if (this.notificationType != null &&
this.notificationType.special) {
return Type.SPECIAL_NOTIFICATIONS;
} else {
return type;
}
}
public boolean shouldRemove() {
return this.notificationType != null ? this.notificationType.shouldRemove() :
this.moduleInfo == null && (this.repoModule == null);
}
public void getButtons(Context context, List<ActionButtonType> buttonTypeList, boolean showcaseMode) {
if (!this.isModuleHolder()) return;
if (this.moduleInfo != null && !showcaseMode) {
buttonTypeList.add(ActionButtonType.UNINSTALL);
}
if (this.repoModule != null) {
buttonTypeList.add(ActionButtonType.INFO);
}
if (this.repoModule != null && !showcaseMode &&
InstallerInitializer.peekMagiskPath() != null) {
buttonTypeList.add(ActionButtonType.UPDATE_INSTALL);
}
String config = this.getMainModuleConfig();
if (config != null) {
String pkg = IntentHelper.getPackageOfConfig(config);
try {
context.getPackageManager().getPackageInfo(pkg, 0);
buttonTypeList.add(ActionButtonType.CONFIG);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Config package \"" + pkg +
"\" missing for module \"" + this.moduleId + "\"");
}
}
ModuleInfo moduleInfo = this.getMainModuleInfo();
if (moduleInfo.support != null) {
buttonTypeList.add(ActionButtonType.SUPPORT);
}
if (moduleInfo.donate != null) {
buttonTypeList.add(ActionButtonType.DONATE);
}
}
public boolean hasUpdate() {
return this.moduleInfo != null && this.repoModule != null &&
this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode;
}
@Override
public int compareTo(ModuleHolder o) {
// Compare depend on type, also allow type spoofing
Type selfTypeReal = this.getType();
Type otherTypeReal = o.getType();
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);
}
public enum Type implements Comparator<ModuleHolder> {
SEPARATOR(R.string.loading, false) {
@Override
@SuppressWarnings("ConstantConditions")
public int compare(ModuleHolder o1, ModuleHolder o2) {
return o1.separator.compareTo(o2.separator);
}
},
NOTIFICATION(R.string.loading, true) {
@Override
@SuppressWarnings("ConstantConditions")
public int compare(ModuleHolder o1, ModuleHolder o2) {
return o1.notificationType.compareTo(o2.notificationType);
}
},
UPDATABLE(R.string.updatable, true) {
@Override
public int compare(ModuleHolder o1, ModuleHolder o2) {
return Long.compare(o2.repoModule.lastUpdated, o1.repoModule.lastUpdated);
}
},
INSTALLED(R.string.installed, true) {
@Override
public int compare(ModuleHolder o1, ModuleHolder o2) {
return o1.getMainModuleName().compareTo(o2.getMainModuleName());
}
},
SPECIAL_NOTIFICATIONS(R.string.loading, true),
INSTALLABLE(R.string.online_repo, true) {
@Override
public int compare(ModuleHolder o1, ModuleHolder o2) {
return Long.compare(o2.repoModule.lastUpdated, o1.repoModule.lastUpdated);
}
},
FOOTER(R.string.loading, false);
@StringRes
public final int title;
public final boolean hasBackground;
Type(@StringRes int title, boolean hasBackground) {
this.title = title;
this.hasBackground = hasBackground;
}
// Note: This method should only be called if both element have the same type
@Override
public int compare(ModuleHolder o1, ModuleHolder o2) {
return 0;
}
}
@Override
public String toString() {
return "ModuleHolder{" +
"moduleId='" + moduleId + '\'' +
", notificationType=" + notificationType +
", separator=" + separator +
", footerPx=" + footerPx +
'}';
}
}

@ -0,0 +1,271 @@
package com.fox2code.mmm;
import android.annotation.SuppressLint;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.util.ArrayList;
public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdapter.ViewHolder> {
private static final boolean DEBUG = false;
public final ArrayList<ModuleHolder> moduleHolders = new ArrayList<>();
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.module_entry, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
final ModuleHolder moduleHolder = this.moduleHolders.get(position);
if (holder.update(moduleHolder)) {
UiThreadHandler.handler.post(() -> {
if (this.moduleHolders.get(position) == moduleHolder) {
this.moduleHolders.remove(position);
this.notifyItemRemoved(position);
}
});
}
}
@Override
public int getItemCount() {
return this.moduleHolders.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
private final CardView cardView;
private final ImageButton buttonAction;
private final SwitchMaterial switchMaterial;
private final TextView titleText;
private final TextView creditText;
private final TextView descriptionText;
private final TextView updateText;
private final ImageButton[] actionsButtons;
private final ArrayList<ActionButtonType> actionButtonsTypes;
private boolean initState;
public ModuleHolder moduleHolder;
public Drawable background;
public ViewHolder(@NonNull View itemView) {
super(itemView);
this.initState = true;
this.cardView = itemView.findViewById(R.id.card_view);
this.buttonAction = itemView.findViewById(R.id.button_action);
this.switchMaterial = itemView.findViewById(R.id.switch_action);
this.titleText = itemView.findViewById(R.id.title_text);
this.creditText = itemView.findViewById(R.id.credit_text);
this.descriptionText = itemView.findViewById(R.id.description_text);
this.updateText = itemView.findViewById(R.id.updated_text);
this.actionsButtons = new ImageButton[6];
this.actionsButtons[0] = itemView.findViewById(R.id.button_action1);
this.actionsButtons[1] = itemView.findViewById(R.id.button_action2);
this.actionsButtons[2] = itemView.findViewById(R.id.button_action3);
this.actionsButtons[3] = itemView.findViewById(R.id.button_action4);
this.actionsButtons[4] = itemView.findViewById(R.id.button_action5);
this.actionsButtons[5] = itemView.findViewById(R.id.button_action6);
this.background = this.cardView.getBackground();
// Apply default
this.cardView.setOnClickListener(v -> {
ModuleHolder moduleHolder = this.moduleHolder;
if (moduleHolder != null &&
moduleHolder.notificationType != null) {
View.OnClickListener onClickListener =
moduleHolder.notificationType.onClickListener;
if (onClickListener != null)
onClickListener.onClick(v);
}
});
this.switchMaterial.setEnabled(false);
this.switchMaterial.setOnCheckedChangeListener((v, checked) -> {
if (this.initState) return; // Skip if non user
ModuleHolder moduleHolder = this.moduleHolder;
if (moduleHolder != null && moduleHolder.moduleInfo != null) {
ModuleInfo moduleInfo = moduleHolder.moduleInfo;
if (!ModuleManager.getINSTANCE().setEnabledState(moduleInfo, checked)) {
this.switchMaterial.setChecked( // Reset to valid state if action failed
(moduleInfo.flags & ModuleInfo.FLAG_MODULE_DISABLED) == 0);
}
}
});
this.actionButtonsTypes = new ArrayList<>();
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
ModuleHolder moduleHolder = this.moduleHolder;
if (index < this.actionButtonsTypes.size() && moduleHolder != null) {
this.actionButtonsTypes.get(index)
.doAction((ImageButton) v, moduleHolder);
if (moduleHolder.shouldRemove()) {
this.cardView.setVisibility(View.GONE);
}
}
});
this.actionsButtons[i].setOnLongClickListener(v -> {
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((ImageButton) v, moduleHolder);
if (moduleHolder.shouldRemove()) {
this.cardView.setVisibility(View.GONE);
}
}
return didSomething;
});
}
this.initState = false;
}
@SuppressLint("SetTextI18n")
public boolean update(ModuleHolder moduleHolder) {
this.initState = true;
if (moduleHolder.isModuleHolder() && moduleHolder.shouldRemove()) {
this.cardView.setVisibility(View.GONE);
this.moduleHolder = null;
this.initState = false;
return true;
}
ModuleHolder.Type type = moduleHolder.getType();
ModuleHolder.Type vType = moduleHolder.getCompareType(type);
this.cardView.setVisibility(View.VISIBLE);
boolean showCaseMode = MainApplication.isShowcaseMode();
if (moduleHolder.isModuleHolder()) {
this.buttonAction.setVisibility(View.GONE);
ModuleInfo localModuleInfo = moduleHolder.moduleInfo;
if (localModuleInfo != null) {
this.switchMaterial.setVisibility(View.VISIBLE);
this.switchMaterial.setChecked((localModuleInfo.flags &
ModuleInfo.FLAG_MODULE_DISABLED) == 0);
} else {
this.switchMaterial.setVisibility(View.GONE);
}
this.creditText.setVisibility(View.VISIBLE);
this.descriptionText.setVisibility(View.VISIBLE);
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
this.titleText.setText(moduleInfo.name);
this.creditText.setText(moduleInfo.version + " by " + moduleInfo.author);
this.descriptionText.setText(moduleInfo.description);
String updateText = moduleHolder.getUpdateTimeText();
if (!updateText.isEmpty()) {
this.updateText.setVisibility(View.VISIBLE);
this.updateText.setText(this.updateText.getContext()
.getString(R.string.last_updated) + " " + updateText);
} else if (moduleHolder.moduleId.equals("hosts")) {
this.updateText.setVisibility(View.VISIBLE);
this.updateText.setText(R.string.magisk_builtin_module);
} else {
this.updateText.setVisibility(View.GONE);
}
this.actionButtonsTypes.clear();
moduleHolder.getButtons(itemView.getContext(), this.actionButtonsTypes, showCaseMode);
this.switchMaterial.setEnabled(!showCaseMode &&
!moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING));
for (int i = 0; i < this.actionsButtons.length; i++) {
ImageButton imageButton = this.actionsButtons[i];
if (i < this.actionButtonsTypes.size()) {
imageButton.setVisibility(View.VISIBLE);
this.actionButtonsTypes.get(i)
.update(imageButton, moduleHolder);
} else {
imageButton.setVisibility(View.GONE);
}
}
this.cardView.setClickable(false);
if (moduleHolder.isModuleHolder() &&
moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_ACTIVE)) {
this.titleText.setTypeface(Typeface.DEFAULT_BOLD);
} else {
this.titleText.setTypeface(Typeface.DEFAULT);
}
} else {
this.buttonAction.setVisibility(
type == ModuleHolder.Type.NOTIFICATION ?
View.VISIBLE : View.GONE);
this.switchMaterial.setVisibility(View.GONE);
this.creditText.setVisibility(View.GONE);
this.descriptionText.setVisibility(View.GONE);
this.updateText.setVisibility(View.GONE);
this.titleText.setText(" ");
this.creditText.setText(" ");
this.descriptionText.setText(" ");
this.switchMaterial.setEnabled(false);
this.actionButtonsTypes.clear();
for (ImageButton button:this.actionsButtons) {
button.setVisibility(View.GONE);
}
if (type == ModuleHolder.Type.NOTIFICATION) {
NotificationType notificationType = moduleHolder.notificationType;
this.titleText.setText(notificationType.textId);
this.buttonAction.setImageResource(notificationType.iconId);
this.cardView.setClickable(notificationType.onClickListener != null);
this.titleText.setTypeface(notificationType.special ?
Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
} else {
this.cardView.setClickable(false);
this.titleText.setTypeface(Typeface.DEFAULT);
}
}
if (type == ModuleHolder.Type.SEPARATOR) {
this.titleText.setText(moduleHolder.separator.title);
}
if (DEBUG) {
this.titleText.setText(this.titleText.getText() + " " +
formatType(type) + " " + formatType(vType));
}
// Coloration system
Drawable drawable = this.cardView.getBackground();
if (drawable != null) this.background = drawable;
if (type.hasBackground) {
if (drawable == null) {
this.cardView.setBackground(this.background);
}
int backgroundAttr = R.attr.colorBackgroundFloating;
if (type == ModuleHolder.Type.NOTIFICATION) {
backgroundAttr = moduleHolder.notificationType.backgroundAttr;
}
Resources.Theme theme = this.cardView.getContext().getTheme();
TypedValue value = new TypedValue();
theme.resolveAttribute(backgroundAttr, value, true);
this.cardView.setCardBackgroundColor(value.data);
} else {
this.cardView.setBackground(null);
}
if (type == ModuleHolder.Type.FOOTER) {
this.titleText.setMinHeight(moduleHolder.footerPx);
} else {
this.titleText.setMinHeight(0);
}
this.moduleHolder = moduleHolder;
this.initState = false;
return false;
}
}
private static String formatType(ModuleHolder.Type type) {
return type.name().substring(0, 3) + "_" + type.ordinal();
}
}

@ -0,0 +1,244 @@
package com.fox2code.mmm;
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.installer.InstallerInitializer;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
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;
public class ModuleViewListBuilder {
private static final String TAG = "ModuleViewListBuilder";
private final EnumSet<NotificationType> notifications = EnumSet.noneOf(NotificationType.class);
private final HashMap<String, ModuleHolder> mappedModuleHolders = new HashMap<>();
private final Object updateLock = new Object();
private final Object queryLock = new Object();
private final Activity activity;
@NonNull
private String query = "";
private int footerPx;
private boolean noUpdate;
public ModuleViewListBuilder(Activity activity) {
this.activity = activity;
}
public void addNotification(NotificationType notificationType) {
synchronized (this.updateLock) {
this.notifications.add(notificationType);
}
}
public void appendInstalledModules() {
synchronized (this.updateLock) {
for (ModuleHolder moduleHolder : this.mappedModuleHolders.values()) {
moduleHolder.moduleInfo = null;
}
ModuleManager moduleManager = ModuleManager.getINSTANCE();
moduleManager.runAfterScan(() -> {
Log.i(TAG, "A1: " + moduleManager.getModules().size());
for (ModuleInfo moduleInfo : moduleManager.getModules().values()) {
ModuleHolder moduleHolder = this.mappedModuleHolders.get(moduleInfo.id);
if (moduleHolder == null) {
this.mappedModuleHolders.put(moduleInfo.id,
moduleHolder = new ModuleHolder(moduleInfo.id));
}
moduleHolder.moduleInfo = moduleInfo;
}
});
}
}
public void appendRemoteModules() {
synchronized (this.updateLock) {
boolean showIncompatible = MainApplication.isShowIncompatibleModules();
for (ModuleHolder moduleHolder : this.mappedModuleHolders.values()) {
moduleHolder.repoModule = null;
}
RepoManager repoManager = RepoManager.getINSTANCE();
repoManager.runAfterUpdate(() -> {
Log.i(TAG, "A2: " + repoManager.getModules().size());
for (RepoModule repoModule : repoManager.getModules().values()) {
if (!showIncompatible && (repoModule.moduleInfo.minApi > Build.VERSION.SDK_INT ||
// Only check Magisk compatibility if root is present
(InstallerInitializer.peekMagiskPath() != null &&
repoModule.moduleInfo.minMagisk >
InstallerInitializer.peekMagiskVersion()
)))
continue; // Skip adding incompatible modules
ModuleHolder moduleHolder = this.mappedModuleHolders.get(repoModule.id);
if (moduleHolder == null) {
this.mappedModuleHolders.put(repoModule.id,
moduleHolder = new ModuleHolder(repoModule.id));
}
moduleHolder.repoModule = repoModule;
}
});
}
}
public void applyTo(RecyclerView moduleList, ModuleViewAdapter moduleViewAdapter) {
if (this.noUpdate) return;
this.noUpdate = true;
final ArrayList<ModuleHolder> moduleHolders;
final int newNotificationsLen;
try {
synchronized (this.updateLock) {
// Build start
moduleHolders = new ArrayList<>();
int special = 0;
Iterator<NotificationType> notificationTypeIterator = this.notifications.iterator();
while (notificationTypeIterator.hasNext()) {
NotificationType notificationType = notificationTypeIterator.next();
if (notificationType.shouldRemove()) {
notificationTypeIterator.remove();
} else {
if (notificationType.special) special++;
moduleHolders.add(new ModuleHolder(notificationType));
}
}
newNotificationsLen = this.notifications.size() - special;
EnumSet<ModuleHolder.Type> headerTypes = EnumSet.of(
ModuleHolder.Type.NOTIFICATION, ModuleHolder.Type.SEPARATOR);
Iterator<ModuleHolder> moduleHolderIterator = this.mappedModuleHolders.values().iterator();
synchronized (this.queryLock) {
while (moduleHolderIterator.hasNext()) {
ModuleHolder moduleHolder = moduleHolderIterator.next();
if (moduleHolder.shouldRemove()) {
moduleHolderIterator.remove();
} else {
ModuleHolder.Type type = moduleHolder.getType();
if (matchFilter(moduleHolder)) {
if (headerTypes.add(type)) {
moduleHolders.add(new ModuleHolder(type));
}
moduleHolders.add(moduleHolder);
}
}
}
}
Collections.sort(moduleHolders, ModuleHolder::compareTo);
Log.i(TAG, "Got " + moduleHolders.size() + " entries!");
// Build end
}
} finally {
this.noUpdate = false;
}
this.activity.runOnUiThread(() -> {
final EnumSet<NotificationType> oldNotifications =
EnumSet.noneOf(NotificationType.class);
if (this.footerPx != 0) {
moduleHolders.add(new ModuleHolder(this.footerPx));
}
boolean isTop = !moduleList.canScrollVertically(-1);
boolean isBottom = !isTop && !moduleList.canScrollVertically(1);
int oldNotificationsLen = 0;
int oldOfflineModulesLen = 0;
for (ModuleHolder moduleHolder : moduleViewAdapter.moduleHolders) {
NotificationType notificationType = moduleHolder.notificationType;
if (notificationType != null) {
oldNotifications.add(notificationType);
if (!notificationType.special)
oldNotificationsLen++;
}
if (moduleHolder.separator == ModuleHolder.Type.INSTALLABLE)
break;
oldOfflineModulesLen++;
}
oldOfflineModulesLen -= oldNotificationsLen;
int newOfflineModulesLen = 0;
for (ModuleHolder moduleHolder : moduleHolders) {
if (moduleHolder.separator == ModuleHolder.Type.INSTALLABLE)
break;
newOfflineModulesLen++;
}
newOfflineModulesLen -= newNotificationsLen;
moduleViewAdapter.moduleHolders.size();
int newLen = moduleHolders.size();
int oldLen = moduleViewAdapter.moduleHolders.size();
moduleViewAdapter.moduleHolders.clear();
moduleViewAdapter.moduleHolders.addAll(moduleHolders);
if (oldNotificationsLen != newNotificationsLen ||
!oldNotifications.equals(this.notifications)) {
notifySizeChanged(moduleViewAdapter, 0,
oldNotificationsLen, newNotificationsLen);
}
if (newLen - newNotificationsLen == 0) {
notifySizeChanged(moduleViewAdapter, newNotificationsLen,
oldLen - oldNotificationsLen, 0);
} else {
notifySizeChanged(moduleViewAdapter, newNotificationsLen,
oldOfflineModulesLen, newOfflineModulesLen);
notifySizeChanged(moduleViewAdapter,
newNotificationsLen + newOfflineModulesLen,
oldLen - oldNotificationsLen - oldOfflineModulesLen,
newLen - newNotificationsLen - newOfflineModulesLen);
}
if (isTop) moduleList.scrollToPosition(0);
if (isBottom) moduleList.scrollToPosition(newLen);
});
}
public void setFooterPx(int footerPx) {
this.footerPx = Math.max(footerPx, 0);
}
private boolean matchFilter(ModuleHolder moduleHolder) {
if (this.query.isEmpty()) return true;
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
return moduleInfo.id.toLowerCase(Locale.ROOT).contains(this.query) ||
moduleInfo.name.toLowerCase(Locale.ROOT).contains(this.query);
}
private static void notifySizeChanged(ModuleViewAdapter moduleViewAdapter,
int index, int oldLen, int newLen) {
// Log.i(TAG, "A: " + index + " " + oldLen + " " + newLen);
if (oldLen == newLen) {
if (newLen != 0)
moduleViewAdapter.notifyItemRangeChanged(index, newLen);
} else if (oldLen < newLen) {
if (oldLen != 0)
moduleViewAdapter.notifyItemRangeChanged(index, oldLen);
moduleViewAdapter.notifyItemRangeInserted(
index + oldLen, newLen - oldLen);
} else {
if (newLen != 0)
moduleViewAdapter.notifyItemRangeChanged(index, newLen);
moduleViewAdapter.notifyItemRangeRemoved(
index + newLen, oldLen - newLen);
}
}
public void setQuery(String query) {
synchronized (this.queryLock) {
Log.i(TAG, "Query " + this.query + " -> " + query);
this.query = query == null ? "" :
query.trim().toLowerCase(Locale.ROOT);
}
}
public boolean setQueryChange(String query) {
synchronized (this.queryLock) {
String newQuery = query == null ? "" :
query.trim().toLowerCase(Locale.ROOT);
Log.i(TAG, "Query change " + this.query + " -> " + newQuery);
if (this.query.equals(newQuery))
return false;
this.query = newQuery;
}
return true;
}
}

@ -0,0 +1,134 @@
package com.fox2code.mmm;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.AttrRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.IntentHelper;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipFile;
interface NotificationTypeCst {
String TAG = "NotificationType";
}
public enum NotificationType implements NotificationTypeCst {
SHOWCASE_MODE(R.string.showcase_mode, R.drawable.ic_baseline_monitor_24,
R.attr.colorPrimary, R.attr.colorOnPrimary) {
@Override
public boolean shouldRemove() {
return !MainApplication.isShowcaseMode();
}
},
NO_ROOT(R.string.fail_root_magisk, R.drawable.ic_baseline_numbers_24) {
@Override
public boolean shouldRemove() {
return InstallerInitializer.peekMagiskPath() != null;
}
},
MAGISK_OUTDATED(R.string.magisk_outdated, R.drawable.ic_baseline_update_24) {
@Override
public boolean shouldRemove() {
return InstallerInitializer.peekMagiskPath() == null ||
InstallerInitializer.peekMagiskVersion() >=
Constants.MAGISK_VER_CODE_PATH_SUPPORT;
}
},
NO_INTERNET(R.string.fail_internet, R.drawable.ic_baseline_cloud_off_24) {
@Override
public boolean shouldRemove() {
return RepoManager.getINSTANCE().hasConnectivity();
}
},
INSTALL_FROM_STORAGE(R.string.install_from_storage, R.drawable.ic_baseline_storage_24,
R.attr.colorBackgroundFloating, R.attr.colorOnBackground, v -> {
CompatActivity compatActivity = CompatActivity.getCompatActivity(v);
final File module = new File(compatActivity.getCacheDir(),
"installer" + File.separator + "module.zip");
IntentHelper.openFileTo(compatActivity, module, (d, s) -> {
if (s) {
try {
boolean needPatch;
try (ZipFile zipFile = new ZipFile(d)) {
needPatch = zipFile.getEntry("module.prop") == null;
}
if (needPatch) {
Files.patchModuleSimple(Files.read(d),
new FileOutputStream(d));
}
try (ZipFile zipFile = new ZipFile(d)) {
needPatch = zipFile.getEntry("module.prop") == null;
}
if (needPatch) {
if (d.exists() && !d.delete())
Log.w(TAG, "Failed to delete non module zip");
Toast.makeText(compatActivity,
R.string.invalid_format, Toast.LENGTH_SHORT).show();
} else {
IntentHelper.openInstaller(compatActivity, d.getAbsolutePath(),
compatActivity.getString(
R.string.local_install_title), null);
}
} catch (IOException ignored) {
if (d.exists() && !d.delete())
Log.w(TAG, "Failed to delete invalid module");
Toast.makeText(compatActivity,
R.string.invalid_format, Toast.LENGTH_SHORT).show();
}
}
});
}, true) {
@Override
public boolean shouldRemove() {
return MainApplication.isShowcaseMode() ||
InstallerInitializer.peekMagiskPath() == null;
}
};
@StringRes
public final int textId;
@DrawableRes
public final int iconId;
@AttrRes
public final int backgroundAttr;
@AttrRes
public final int foregroundAttr;
public final View.OnClickListener onClickListener;
public final boolean special;
NotificationType(@StringRes int textId, int iconId) {
this(textId, iconId, R.attr.colorError, R.attr.colorOnError);
}
NotificationType(@StringRes int textId, int iconId, int backgroundAttr, int foregroundAttr) {
this(textId, iconId, backgroundAttr, foregroundAttr, null);
}
NotificationType(@StringRes int textId, int iconId, int backgroundAttr, int foregroundAttr, View.OnClickListener onClickListener) {
this(textId, iconId, backgroundAttr, foregroundAttr, null, false);
}
NotificationType(@StringRes int textId, int iconId, int backgroundAttr, int foregroundAttr, View.OnClickListener onClickListener, boolean special) {
this.textId = textId;
this.iconId = iconId;
this.backgroundAttr = backgroundAttr;
this.foregroundAttr = foregroundAttr;
this.onClickListener = onClickListener;
this.special = special;
}
public boolean shouldRemove() {
return false;
}
}

@ -0,0 +1,259 @@
package com.fox2code.mmm.compat;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.CallSuper;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.R;
import java.util.Objects;
/**
* I will probably outsource this to a separate library
*/
public class CompatActivity extends AppCompatActivity {
public static final int INTENT_ACTIVITY_REQUEST_CODE = 0x01000000;
private static final String TAG = "CompatActivity";
public static final CompatActivity.OnBackPressedCallback DISABLE_BACK_BUTTON =
new CompatActivity.OnBackPressedCallback() {
@Override
public boolean onBackPressed(CompatActivity compatActivity) {
compatActivity.setOnBackPressedCallback(this);
return true;
}
};
private CompatActivity.OnActivityResultCallback onActivityResultCallback;
private CompatActivity.OnBackPressedCallback onBackPressedCallback;
private MenuItem.OnMenuItemClickListener menuClickListener;
@StyleRes private int setThemeDynamic = 0;
private boolean onCreateCalled = false;
private boolean isRefreshUi = false;
private int drawableResId;
MenuItem menuItem;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
Application application = this.getApplication();
if (application instanceof ApplicationCallbacks) {
((ApplicationCallbacks) application).onCreateCompatActivity(this);
}
super.onCreate(savedInstanceState);
this.onCreateCalled = true;
}
@Override
protected void onResume() {
super.onResume();
this.refreshUI();
}
@Override
public void finish() {
this.onActivityResultCallback = null;
boolean fadeOut = this.onCreateCalled && this.getIntent()
.getBooleanExtra(Constants.EXTRA_FADE_OUT, false);
super.finish();
if (fadeOut) {
super.overridePendingTransition(
android.R.anim.fade_in, android.R.anim.fade_out);
}
}
@CallSuper
public void refreshUI() {
// Avoid recursive calls
if (this.isRefreshUi) return;
Application application = this.getApplication();
if (application instanceof ApplicationCallbacks) {
this.isRefreshUi = true;
try {
((ApplicationCallbacks) application)
.onRefreshUI(this);
} finally {
this.isRefreshUi = false;
}
}
}
public final void forceBackPressed() {
if (!this.isFinishing())
super.onBackPressed();
}
@Override
public void onBackPressed() {
if (this.isFinishing()) return;
OnBackPressedCallback onBackPressedCallback = this.onBackPressedCallback;
this.onBackPressedCallback = null;
if (onBackPressedCallback == null ||
!onBackPressedCallback.onBackPressed(this)) {
super.onBackPressed();
}
}
public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) {
androidx.appcompat.app.ActionBar compatActionBar = this.getSupportActionBar();
if (compatActionBar != null) {
compatActionBar.setDisplayHomeAsUpEnabled(showHomeAsUp);
} else {
android.app.ActionBar actionBar = this.getActionBar();
if (actionBar != null)
actionBar.setDisplayHomeAsUpEnabled(showHomeAsUp);
}
}
public void setActionBarExtraMenuButton(@DrawableRes int drawableResId,
MenuItem.OnMenuItemClickListener menuClickListener) {
Objects.requireNonNull(menuClickListener);
this.drawableResId = drawableResId;
this.menuClickListener = menuClickListener;
if (this.menuItem != null) {
this.menuItem.setOnMenuItemClickListener(this.menuClickListener);
this.menuItem.setIcon(this.drawableResId);
this.menuItem.setEnabled(true);
}
}
public void removeActionBarExtraMenuButton() {
this.drawableResId = 0;
this.menuClickListener = null;
if (this.menuItem != null) {
this.menuItem.setOnMenuItemClickListener(null);
this.menuItem.setIcon(null);
this.menuItem.setEnabled(false);
}
}
// like setTheme but recreate the activity if needed
public void setThemeRecreate(@StyleRes int resId) {
if (!this.onCreateCalled) {
this.setTheme(resId);
return;
}
if (this.setThemeDynamic == resId)
return;
if (this.setThemeDynamic != 0)
throw new IllegalStateException("setThemeDynamic called recursively");
this.setThemeDynamic = resId;
try {
super.setTheme(resId);
} finally {
this.setThemeDynamic = 0;
}
}
@Override
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
super.onApplyThemeResource(theme, resid, first);
if (resid != 0 && this.setThemeDynamic == resid) {
Activity parent = this.getParent();
(parent == null ? this : parent).recreate();
super.overridePendingTransition(
android.R.anim.fade_in, android.R.anim.fade_out);
}
}
public void setOnBackPressedCallback(OnBackPressedCallback onBackPressedCallback) {
this.onBackPressedCallback = onBackPressedCallback;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
androidx.appcompat.app.ActionBar compatActionBar = this.getSupportActionBar();
android.app.ActionBar actionBar = this.getActionBar();
if (compatActionBar != null ? (compatActionBar.getDisplayOptions() &
androidx.appcompat.app.ActionBar.DISPLAY_HOME_AS_UP) != 0 :
actionBar != null && (actionBar.getDisplayOptions() &
android.app.ActionBar.DISPLAY_HOME_AS_UP) != 0) {
this.onBackPressed();
return true;
}
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
this.getMenuInflater().inflate(R.menu.compat_menu, menu);
this.menuItem = menu.findItem(R.id.compat_menu_item);
if (this.menuClickListener != null) {
this.menuItem.setOnMenuItemClickListener(this.menuClickListener);
this.menuItem.setIcon(this.drawableResId);
this.menuItem.setEnabled(true);
}
return super.onCreateOptionsMenu(menu);
}
@SuppressWarnings("deprecation")
public void startActivityForResult(Intent intent, @Nullable Bundle options,
OnActivityResultCallback onActivityResultCallback) {
super.startActivityForResult(intent, INTENT_ACTIVITY_REQUEST_CODE, options);
this.onActivityResultCallback = onActivityResultCallback;
}
@Override
@CallSuper
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == INTENT_ACTIVITY_REQUEST_CODE) {
OnActivityResultCallback callback = this.onActivityResultCallback;
if (callback != null) {
this.onActivityResultCallback = null;
callback.onActivityResult(resultCode, data);
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
public static CompatActivity getCompatActivity(View view) {
return getCompatActivity(view.getContext());
}
public static CompatActivity getCompatActivity(Fragment fragment) {
return getCompatActivity(fragment.getContext());
}
public static CompatActivity getCompatActivity(Context context) {
while (!(context instanceof CompatActivity)) {
if (context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
} else return null;
}
return (CompatActivity) context;
}
@FunctionalInterface
public interface OnActivityResultCallback {
void onActivityResult(int resultCode, @Nullable Intent data);
}
@FunctionalInterface
public interface OnBackPressedCallback {
boolean onBackPressed(CompatActivity compatActivity);
}
public interface ApplicationCallbacks {
void onCreateCompatActivity(CompatActivity compatActivity);
void onRefreshUI(CompatActivity compatActivity);
}
}

@ -0,0 +1,327 @@
package com.fox2code.mmm.installer;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
import com.fox2code.mmm.ActionButtonType;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.topjohnwu.superuser.CallbackList;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class InstallerActivity extends CompatActivity {
private static final String TAG = "InstallerActivity";
public LinearProgressIndicator progressIndicator;
public InstallerTerminal installerTerminal;
private File moduleCache;
private File toDelete;
@Override
protected void onCreate(Bundle savedInstanceState) {
this.moduleCache = new File(this.getCacheDir(), "installer");
if (!this.moduleCache.exists() && !this.moduleCache.mkdirs())
Log.e(TAG, "Failed to mkdir module cache dir!");
this.setDisplayHomeAsUpEnabled(false);
this.setOnBackPressedCallback(DISABLE_BACK_BUTTON);
super.onCreate(savedInstanceState);
final Intent intent = this.getIntent();
final String target;
final String name;
// Should we allow 3rd part app to install modules?
if (Constants.INTENT_INSTALL_INTERNAL.equals(intent.getAction())) {
if (!MainApplication.checkSecret(intent)) {
Log.e(TAG, "Security check failed!");
this.forceBackPressed();
return;
}
target = intent.getExtras().getString(Constants.EXTRA_INSTALL_PATH);
name = intent.getExtras().getString(Constants.EXTRA_INSTALL_NAME);
} else {
Toast.makeText(this, "Unknown intent!", Toast.LENGTH_SHORT).show();
this.forceBackPressed();
return;
}
boolean urlMode = target.startsWith("http://") || target.startsWith("https://");
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setTitle(name);
setContentView(R.layout.installer);
this.progressIndicator = findViewById(R.id.progress_bar);
this.installerTerminal = new InstallerTerminal(findViewById(R.id.install_terminal));
this.progressIndicator.setVisibility(View.GONE);
this.progressIndicator.setIndeterminate(true);
if (urlMode) {
this.progressIndicator.setVisibility(View.VISIBLE);
this.installerTerminal.addLine("- Downloading " + name);
new Thread(() -> {
File moduleCache = this.toDelete =
new File(this.moduleCache, "module.zip");
if (moduleCache.exists() && !moduleCache.delete() &&
!new SuFile(moduleCache.getAbsolutePath()).delete())
Log.e(TAG, "Failed to delete module cache");
try {
Log.i(TAG, "Downloading: " + target);
byte[] rawModule = Http.doHttpGet(target,(progress, max, done) -> {
if (max <= 0 && this.progressIndicator.isIndeterminate())
return;
this.runOnUiThread(() -> {
this.progressIndicator.setIndeterminate(false);
this.progressIndicator.setMax(max);
this.progressIndicator.setProgressCompat(progress, true);
});
});
this.runOnUiThread(() -> {
this.installerTerminal.addLine("- Patching " + name);
this.progressIndicator.setVisibility(View.GONE);
this.progressIndicator.setIndeterminate(true);
});
Log.i(TAG, "Patching: " + moduleCache.getName());
try (OutputStream outputStream = new FileOutputStream(moduleCache)) {
Files.patchModuleSimple(rawModule, outputStream);
outputStream.flush();
} finally {
//noinspection UnusedAssignment (Important for GC)
rawModule = null;
}
this.runOnUiThread(() -> {
this.installerTerminal.addLine("- Installing " + name);
});
this.doInstall(moduleCache);
} catch (IOException e) {
Log.e(TAG, "Failed to download module zip", e);
this.setInstallStateFinished(false,
"! Failed to download module zip", "");
}
}, "Module download Thread").start();
} else {
this.installerTerminal.addLine("- Installing " + name);
new Thread(() -> this.doInstall(
this.toDelete = new File(target)),
"Install Thread").start();
}
}
private void doInstall(File file) {
Log.i(TAG, "Installing: " + moduleCache.getName());
File installScript = this.extractCompatScript();
if (installScript == null) {
this.setInstallStateFinished(false,
"! Failed to extract module install script", "");
return;
}
InstallerController installerController = new InstallerController(
this.progressIndicator, this.installerTerminal);
InstallerMonitor installerMonitor = new InstallerMonitor(installScript);
boolean success = Shell.su("export MMM_EXT_SUPPORT=1",
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
"sh \"" + installScript.getAbsolutePath() + "\"" +
" /dev/null 1 \"" + file.getAbsolutePath() + "\"")
.to(installerController, installerMonitor).exec().isSuccess();
installerController.disable();
String message = "- Install successful";
if (!success) {
message = installerMonitor.doCleanUp();
}
this.setInstallStateFinished(success, message,
installerController.getSupportLink());
}
public static class InstallerController extends CallbackList<String> {
private final LinearProgressIndicator progressIndicator;
private final InstallerTerminal terminal;
private boolean enabled, useExt;
private String supportLink = "";
private InstallerController(LinearProgressIndicator progressIndicator, InstallerTerminal terminal) {
this.progressIndicator = progressIndicator;
this.terminal = terminal;
this.enabled = true;
this.useExt = false;
}
@Override
public void onAddElement(String s) {
if (!this.enabled) return;
Log.d(TAG, "MSG: " + s);
if ("#!useExt".equals(s)) {
this.useExt = true;
return;
}
if (this.useExt && s.startsWith("#!")) {
this.processCommand(s);
} else {
this.terminal.addLine(s);
}
}
private void processCommand(String rawCommand) {
final String arg;
final String command;
int i = rawCommand.indexOf(' ');
if (i != -1) {
arg = rawCommand.substring(i + 1);
command = rawCommand.substring(2, i);
} else {
arg = "";
command = rawCommand.substring(2);
}
switch (command) {
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.progressIndicator.setVisibility(View.VISIBLE);
break;
case "hideLoading":
this.progressIndicator.setVisibility(View.GONE);
break;
case "setSupportLink":
// Only set link if valid
if (arg.isEmpty() || (arg.startsWith("https://") &&
arg.indexOf('/', 8) > 8))
this.supportLink = arg;
break;
}
}
public void disable() {
this.enabled = false;
}
public String getSupportLink() {
return supportLink;
}
}
@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 installScriptPath;
public String lastCommand;
public InstallerMonitor(File installScript) {
super(Runnable::run);
this.installScriptPath = installScript.getAbsolutePath();
}
@Override
public void onAddElement(String s) {
Log.d(TAG, "Monitor: " + s);
this.lastCommand = s;
}
private String doCleanUp() {
String installScriptErr =
this.installScriptPath + ": /data/adb/modules_update/";
// This block is mainly to help fixing customize.sh syntax errors
if (this.lastCommand.startsWith(installScriptErr)) {
installScriptErr = this.lastCommand.substring(installScriptErr.length());
int i = installScriptErr.indexOf('/');
if (i == -1) return DEFAULT_ERR;
String module = installScriptErr.substring(0, i);
SuFile moduleUpdate = new SuFile("/data/adb/modules_update/" + module);
if (moduleUpdate.exists()) {
if (!moduleUpdate.deleteRecursive())
Log.e(TAG, "Failed to delete failed update");
return "Error: " + installScriptErr.substring(i + 1);
}
}
return DEFAULT_ERR;
}
}
private static boolean didExtract = false;
private File extractCompatScript() {
File compatInstallScript = new File(this.moduleCache, "module_installer_compat.sh");
if (!compatInstallScript.exists() || compatInstallScript.length() == 0 || !didExtract) {
try {
Files.write(compatInstallScript, Files.readAllBytes(
this.getAssets().open("module_installer_compat.sh")));
didExtract = true;
} catch (IOException e) {
compatInstallScript.delete();
Log.e(TAG, "Failed to extract module_installer_compat.sh", e);
return null;
}
}
return compatInstallScript;
}
@SuppressWarnings("SameParameterValue")
private void setInstallStateFinished(boolean success, String message,String optionalLink) {
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.setOnBackPressedCallback(null);
this.setDisplayHomeAsUpEnabled(true);
this.progressIndicator.setVisibility(View.GONE);
if (message != null && !message.isEmpty())
this.installerTerminal.addLine(message);
if (!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 {
this.getPackageManager().getPackageInfo(configPkg, 0);
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");
}
}
}
});
}
}

@ -0,0 +1,121 @@
package com.fox2code.mmm.installer;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
import com.topjohnwu.superuser.NoShellException;
import com.topjohnwu.superuser.Shell;
import java.io.File;
import java.util.ArrayList;
public class InstallerInitializer extends Shell.Initializer {
private static final String TAG = "InstallerInitializer";
private static String MAGISK_PATH;
private static int MAGISK_VERSION_CODE;
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;
public interface Callback {
void onPathReceived(String path);
void onFailure(int error);
}
public static String peekMagiskPath() {
return InstallerInitializer.MAGISK_PATH;
}
public static int peekMagiskVersion() {
return InstallerInitializer.MAGISK_VERSION_CODE;
}
public static void tryGetMagiskPathAsync(Callback callback) {
tryGetMagiskPathAsync(callback, false);
}
public static void tryGetMagiskPathAsync(Callback callback,boolean forceCheck) {
String MAGISK_PATH = InstallerInitializer.MAGISK_PATH;
if (MAGISK_PATH != null && !forceCheck) {
callback.onPathReceived(MAGISK_PATH);
return;
}
Thread thread = new Thread("Magisk GetPath Thread") {
@Override
public void run() {
int error;
String MAGISK_PATH = null;
try {
MAGISK_PATH = tryGetMagiskPath(forceCheck);
error = ERROR_NO_PATH;
} catch (NoShellException e) {
error = ERROR_NO_SU;
Log.w(TAG, "Device don't have root!", e);
} catch (Throwable e) {
error = ERROR_OTHER;
Log.e(TAG, "Something happened", e);
}
if (forceCheck) {
InstallerInitializer.MAGISK_PATH = MAGISK_PATH;
}
if (MAGISK_PATH != null) {
MainApplication.setHasGottenRootAccess(true);
callback.onPathReceived(MAGISK_PATH);
} else {
MainApplication.setHasGottenRootAccess(false);
callback.onFailure(error);
}
}
};
thread.start();
}
private static String tryGetMagiskPath(boolean forceCheck) {
String MAGISK_PATH = InstallerInitializer.MAGISK_PATH;
int MAGISK_VERSION_CODE;
if (MAGISK_PATH != null && !forceCheck) return MAGISK_PATH;
ArrayList<String> output = new ArrayList<>();
if(!Shell.su( "magisk -V", "magisk --path").to(output).exec().isSuccess()) {
return null;
}
MAGISK_PATH = output.size() < 2 ? "" : output.get(1);
MAGISK_VERSION_CODE = Integer.parseInt(output.get(0));
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())) {
MAGISK_PATH = "/sbin";
}
if (MAGISK_PATH.length() != 0 && new File(MAGISK_PATH).exists()) {
InstallerInitializer.MAGISK_PATH = MAGISK_PATH;
} else {
Log.e(TAG, "Failed to get Magisk path (Got " + MAGISK_PATH + ")");
MAGISK_PATH = null;
}
InstallerInitializer.MAGISK_VERSION_CODE = MAGISK_VERSION_CODE;
return MAGISK_PATH;
}
@Override
public boolean onInit(@NonNull Context context, @NonNull Shell shell) {
if (!shell.isRoot())
return true;
Shell.Job newJob = shell.newJob();
String MAGISK_PATH = InstallerInitializer.MAGISK_PATH;
if (MAGISK_PATH == null) {
Log.w(TAG, "Unable to detect magisk path!");
} else {
newJob.add("export ASH_STANDALONE=1");
newJob.add("export PATH=\"" + MAGISK_PATH + "/.magisk/busybox;$PATH\"");
newJob.add("export MAGISKTMP=\"" + MAGISK_PATH + "/.magisk\"");
newJob.add("busybox sh");
}
return true;
}
}

@ -0,0 +1,125 @@
package com.fox2code.mmm.installer;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.TextViewHolder> {
private final RecyclerView recyclerView;
private final ArrayList<String> terminal;
private final Object lock = new Object();
public InstallerTerminal(RecyclerView recyclerView) {
recyclerView.setLayoutManager(
new LinearLayoutManager(recyclerView.getContext()));
this.recyclerView = recyclerView;
this.terminal = new ArrayList<>();
this.recyclerView.setBackground(
new ColorDrawable(Color.BLACK));
this.recyclerView.setAdapter(this);
}
@NonNull
@Override
public TextViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new TextViewHolder(new TextView(parent.getContext()));
}
@Override
public void onBindViewHolder(@NonNull TextViewHolder holder, int position) {
holder.setText(this.terminal.get(position));
}
@Override
public int getItemCount() {
return this.terminal.size();
}
public void addLine(String line) {
synchronized (lock) {
boolean bottom = !this.recyclerView.canScrollVertically(1);
int index = this.terminal.size();
this.terminal.add(line);
this.notifyItemInserted(index);
if (bottom) this.recyclerView.scrollToPosition(index);
}
}
public void setLine(int index, String line) {
synchronized (lock) {
this.terminal.set(index, line);
this.notifyItemChanged(index);
}
}
public void setLastLine(String line) {
synchronized (lock) {
int size = this.terminal.size();
if (size == 0) {
this.terminal.add(line);
this.notifyItemInserted(0);
} else {
this.terminal.set(size - 1, line);
this.notifyItemChanged(size - 1);
}
}
}
public void removeLastLine() {
synchronized (lock) {
int size = this.terminal.size();
if (size != 0) {
this.terminal.remove(size - 1);
this.notifyItemRemoved(size - 1);
}
}
}
public void clearTerminal() {
synchronized (lock) {
int size = this.terminal.size();
if (size != 0) {
this.terminal.clear();
this.notifyItemRangeRemoved(0, size);
}
}
}
public void scrollUp() {
synchronized (lock) {
this.recyclerView.scrollToPosition(0);
}
}
public void scrollDown() {
synchronized (lock) {
this.recyclerView.scrollToPosition(this.terminal.size() - 1);
}
}
public static class TextViewHolder extends RecyclerView.ViewHolder {
private final TextView textView;
public TextViewHolder(@NonNull TextView itemView) {
super(itemView);
this.textView = itemView;
itemView.setTypeface(Typeface.MONOSPACE);
itemView.setTextColor(Color.WHITE);
itemView.setTextSize(12);
itemView.setLines(1);
itemView.setText(" ");
}
private void setText(String text) {
this.textView.setText(text.isEmpty() ? " " : text);
}
}
}

@ -0,0 +1,31 @@
package com.fox2code.mmm.manager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.installer.InstallerInitializer;
public class ModuleBootReceive extends BroadcastReceiver {
private static final String BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || !BOOT_COMPLETED.equals(intent.getAction())
|| !MainApplication.hasGottenRootAccess()) {
return;
}
InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
@Override
public void onPathReceived(String path) {
ModuleManager.getINSTANCE().scan();
}
@Override
public void onFailure(int error) {
MainApplication.setHasGottenRootAccess(false);
}
});
}
}

@ -0,0 +1,42 @@
package com.fox2code.mmm.manager;
/**
* Representation of the module.prop
* Optionally flags represent module status
* It's value is 0 if not applicable
*/
public class ModuleInfo {
public static final int FLAG_MODULE_DISABLED = 0x01;
public static final int FLAG_MODULE_UPDATING = 0x02;
public static final int FLAG_MODULE_ACTIVE = 0x04;
public static final int FLAG_MODULE_UNINSTALLING = 0x08;
public static final int FLAG_MODULE_UPDATING_ONLY = 0x10;
public static final int FLAG_METADATA_INVALID = 0x80000000;
// Magisk standard
public final String id;
public String name;
public String version;
public int versionCode;
public String author;
public String description;
// Community meta
public String support;
public String donate;
public String config;
// Community restrictions
public int minMagisk;
public int minApi;
// Module status (0 if not from Module Manager)
public int flags;
public ModuleInfo(String id) {
this.id = id;
this.name = id;
}
public boolean hasFlag(int flag) {
return (this.flags & flag) != 0;
}
}

@ -0,0 +1,223 @@
package com.fox2code.mmm.manager;
import android.content.SharedPreferences;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.utils.PropUtils;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import java.util.HashMap;
import java.util.Iterator;
public final class ModuleManager {
private static final int FLAG_MM_INVALID = ModuleInfo.FLAG_METADATA_INVALID;
private static final int FLAG_MM_UNPROCESSED = 0x40000000;
private static final int FLAGS_RESET_INIT = FLAG_MM_INVALID |
ModuleInfo.FLAG_MODULE_DISABLED | ModuleInfo.FLAG_MODULE_UPDATING |
ModuleInfo.FLAG_MODULE_UNINSTALLING | ModuleInfo.FLAG_MODULE_ACTIVE;
private static final int FLAGS_RESET_UPDATE = FLAG_MM_INVALID | FLAG_MM_UNPROCESSED;
private final HashMap<String, ModuleInfo> moduleInfos;
private final HashMap<String, ModuleInfo> invalidModules;
private final SharedPreferences bootPrefs;
private final Object scanLock = new Object();
private boolean scanning, lastScanResult;
private static final ModuleManager INSTANCE = new ModuleManager();
public static ModuleManager getINSTANCE() {
return INSTANCE;
}
private ModuleManager() {
this.moduleInfos = new HashMap<>();
this.invalidModules = new HashMap<>();
this.bootPrefs = MainApplication.getBootSharedPreferences();
}
// MultiThread friendly method
public final boolean scan() {
if (!this.scanning) {
// Do scan
synchronized (scanLock) {
this.scanning = true;
try {
this.lastScanResult =
this.scanInternal();
} finally {
this.scanning = false;
}
}
} else {
// Wait for current scan
synchronized (scanLock) {}
}
return this.lastScanResult;
}
// Pause execution until the scan is completed if one is currently running
public final void afterScan() {
if (this.scanning) synchronized (this.scanLock) {}
}
public final void runAfterScan(Runnable runnable) {
synchronized (this.scanLock) {
runnable.run();
}
}
private boolean scanInternal() {
boolean firstScan = this.bootPrefs.getBoolean("mm_first_scan", true);
boolean changed = false;
SharedPreferences.Editor editor = firstScan ? this.bootPrefs.edit() : null;
// Reset existing ModuleInfo
this.moduleInfos.putAll(this.invalidModules);
this.invalidModules.clear();
for (ModuleInfo v : this.moduleInfos.values()) {
v.flags |= FLAG_MM_UNPROCESSED;
v.flags &= ~FLAGS_RESET_INIT;
v.name = v.id;
v.version = null;
v.versionCode = 0;
v.author = null;
v.description = "No description found.";
v.support = null;
v.config = null;
}
String[] modules = new SuFile("/data/adb/modules").list();
if (modules != null) {
for (String module : modules) {
ModuleInfo moduleInfo = moduleInfos.get(module);
if (moduleInfo == null) {
moduleInfo = new ModuleInfo(module);
moduleInfos.put(module, moduleInfo);
changed = true;
// Shis should not really happen, but let's handles theses cases anyway
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UPDATING_ONLY;
}
moduleInfo.flags &= ~FLAGS_RESET_UPDATE;
boolean disabled = new SuFile(
"/data/adb/modules/" + module + "/disable").exists();
if (disabled) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_DISABLED;
} else {
if (firstScan) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_ACTIVE;
editor.putBoolean("module_" + moduleInfo.id + "_active", true);
} else if (bootPrefs.getBoolean("module_" + moduleInfo.id + "_active", false)) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_ACTIVE;
}
}
boolean uninstalling = new SuFile(
"/data/adb/modules/" + module + "/remove").exists();
if (uninstalling) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UNINSTALLING;
}
try {
PropUtils.readProperties(moduleInfo,
"/data/adb/modules/" + module + "/module.prop");
} catch (Exception e) {
moduleInfo.flags |= FLAG_MM_INVALID;
}
}
}
String[] modules_update = new SuFile("/data/adb/modules_update").list();
if (modules_update != null) {
for (String module : modules_update) {
ModuleInfo moduleInfo = moduleInfos.get(module);
if (moduleInfo == null) {
moduleInfo = new ModuleInfo(module);
moduleInfos.put(module, moduleInfo);
changed = true;
}
moduleInfo.flags &= ~FLAGS_RESET_UPDATE;
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UPDATING;
try {
PropUtils.readProperties(moduleInfo,
"/data/adb/modules_update/" + module + "/module.prop");
} catch (Exception e) {
moduleInfo.flags |= FLAG_MM_INVALID;
}
}
}
Iterator<ModuleInfo> moduleInfoIterator =
this.moduleInfos.values().iterator();
while (moduleInfoIterator.hasNext()) {
ModuleInfo moduleInfo = moduleInfoIterator.next();
if ((moduleInfo.flags & FLAG_MM_UNPROCESSED) != 0) {
moduleInfoIterator.remove();
continue; // Don't process fallbacks if unreferenced
} else if ((moduleInfo.flags & FLAG_MM_INVALID) != 0) {
moduleInfo.flags &=~ FLAG_MM_INVALID;
this.invalidModules.put(moduleInfo.id, moduleInfo);
moduleInfoIterator.remove();
}
if (moduleInfo.name == null || (moduleInfo.name.equals(moduleInfo.id))) {
moduleInfo.name = Character.toUpperCase(moduleInfo.id.charAt(0)) +
moduleInfo.id.substring(1).replace('_', ' ');
}
if (moduleInfo.version == null) {
moduleInfo.version = "v" + moduleInfo.versionCode;
}
}
if (firstScan) {
editor.putBoolean("mm_first_scan", false);
editor.apply();
}
return changed;
}
public HashMap<String, ModuleInfo> getModules() {
this.afterScan();
return this.moduleInfos;
}
public HashMap<String, ModuleInfo> getInvalidModules() {
this.afterScan();
return invalidModules;
}
public boolean setEnabledState(ModuleInfo moduleInfo, boolean checked) {
if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING) && !checked) return false;
SuFile disable = new SuFile("/data/adb/modules/" + moduleInfo.id + "/disable");
if (checked) {
if (disable.exists() && !disable.delete()) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_DISABLED;
return false;
}
moduleInfo.flags &= ~ModuleInfo.FLAG_MODULE_DISABLED;
} else {
if (!disable.exists() && !disable.createNewFile()) {
return false;
}
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_DISABLED;
}
return true;
}
public boolean setUninstallState(ModuleInfo moduleInfo, boolean checked) {
if (checked && moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING)) return false;
SuFile disable = new SuFile("/data/adb/modules/" + moduleInfo.id + "/remove");
if (checked) {
if (!disable.exists() && !disable.createNewFile()) {
return false;
}
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UNINSTALLING;
} else {
if (disable.exists() && !disable.delete()) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UNINSTALLING;
return false;
}
moduleInfo.flags &= ~ModuleInfo.FLAG_MODULE_UNINSTALLING;
}
return true;
}
public boolean masterClear(ModuleInfo moduleInfo) {
if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_ACTIVE)) return false;
Shell.su("rm -rf /data/adb/modules/" + moduleInfo.id + "/").exec();
Shell.su("rm -rf /data/adb/modules_update/" + moduleInfo.id + "/").exec();
moduleInfo.flags = ModuleInfo.FLAG_METADATA_INVALID;
return true;
}
}

@ -0,0 +1,81 @@
package com.fox2code.mmm.markdown;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import java.nio.charset.StandardCharsets;
public class MarkdownActivity extends CompatActivity {
private static final String TAG = "MarkdownActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setDisplayHomeAsUpEnabled(true);
Intent intent = this.getIntent();
if (intent == null || !MainApplication.checkSecret(intent)) {
Log.e(TAG, "Impersonation detected!");
this.onBackPressed();
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);
if (title != null && !title.isEmpty()) setTitle(title);
if (config != null && !config.isEmpty()) {
String configPkg = IntentHelper.getPackageOfConfig(config);
try {
this.getPackageManager().getPackageInfo(configPkg, 0);
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 markdown view");
}
}
Log.i(TAG, "Url for markdown " + url);
setContentView(R.layout.markdown_view);
ViewGroup markdownBackground = findViewById(R.id.markdownBackground);
TextView textView = findViewById(R.id.markdownView);
new Thread(() -> {
try {
String markdown = new String(Http.doHttpGet(url, true), StandardCharsets.UTF_8);
Log.i(TAG, "Download successful");
runOnUiThread(() -> {
MainApplication.getINSTANCE().getMarkwon().setMarkdown(textView, markdown);
if (markdownBackground != null) {
markdownBackground.setClickable(true);
markdownBackground.setOnClickListener(v -> this.onBackPressed());
}
});
} catch (Exception e) {
Log.e(TAG, "Failed download", e);
runOnUiThread(() -> {
Toast.makeText(this, R.string.failed_download,
Toast.LENGTH_SHORT).show();
this.onBackPressed();
});
}
}, "Markdown load thread").start();
}
}

@ -0,0 +1,124 @@
package com.fox2code.mmm.repo;
import android.content.SharedPreferences;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.PropUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
public class RepoData {
private final Object populateLock = new Object();
public final String url;
public final File cacheRoot;
public final SharedPreferences cachedPreferences;
public final File metaDataCache;
public final HashMap<String, RepoModule> moduleHashMap;
public long lastUpdate;
public String name;
RepoData(String url, File cacheRoot, SharedPreferences cachedPreferences) {
this.url = url;
this.cacheRoot = cacheRoot;
this.cachedPreferences = cachedPreferences;
this.metaDataCache = new File(cacheRoot, "modules.json");
this.moduleHashMap = new HashMap<>();
this.name = this.url; // Set url as default name
if (!this.cacheRoot.isDirectory()) {
this.cacheRoot.mkdirs();
} else if (this.metaDataCache.exists()) {
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();
}
}
}
List<RepoModule> populate(JSONObject jsonObject) throws JSONException {
List<RepoModule> newModules = new ArrayList<>();
synchronized (this.populateLock) {
String name = jsonObject.getString("name");
long lastUpdate = jsonObject.getLong("last_update");
for (RepoModule repoModule : this.moduleHashMap.values()) {
repoModule.processed = false;
}
JSONArray array = jsonObject.getJSONArray("modules");
int len = array.length();
for (int i = 0; i < len; i++) {
JSONObject module = array.getJSONObject(i);
String moduleId = module.getString("id");
long moduleLastUpdate = module.getLong("last_update");
String moduleNotesUrl = module.getString("notes_url");
String modulePropsUrl = module.getString("prop_url");
String moduleZipUrl = module.getString("zip_url");
RepoModule repoModule = this.moduleHashMap.get(moduleId);
if (repoModule == null) {
repoModule = new RepoModule(moduleId);
this.moduleHashMap.put(moduleId, repoModule);
newModules.add(repoModule);
} else {
if (repoModule.lastUpdated < moduleLastUpdate ||
repoModule.moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
newModules.add(repoModule);
}
}
repoModule.processed = true;
repoModule.lastUpdated = moduleLastUpdate;
repoModule.notesUrl = moduleNotesUrl;
repoModule.propUrl = modulePropsUrl;
repoModule.zipUrl = moduleZipUrl;
}
// Remove no longer existing modules
Iterator<RepoModule> moduleInfoIterator = this.moduleHashMap.values().iterator();
while (moduleInfoIterator.hasNext()) {
RepoModule repoModule = moduleInfoIterator.next();
if (!repoModule.processed) {
new File(this.cacheRoot, repoModule.id + ".prop").delete();
moduleInfoIterator.remove();
}
}
// Update final metadata
this.name = name;
this.lastUpdate = lastUpdate;
}
return newModules;
}
public boolean tryLoadMetadata(RepoModule repoModule) {
File file = new File(this.cacheRoot, repoModule.id + ".prop");
if (file.exists()) {
try {
PropUtils.readProperties(repoModule.moduleInfo, file.getAbsolutePath());
repoModule.moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID;
return true;
} catch (Exception ignored) {
file.delete();
}
}
repoModule.moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID;
return false;
}
public String getNameOrFallback(String fallback) {
return this.name == null ||
this.name.equals(this.url) ?
fallback : this.name;
}
}

@ -0,0 +1,225 @@
package com.fox2code.mmm.repo;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.fox2code.mmm.MainApplication;
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 java.io.File;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
public final class RepoManager {
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_ALT_REPO =
"https://raw.githubusercontent.com/Magisk-Modules-Alt-Repo/json/main/modules.json";
public static final String MAGISK_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Repo";
public static final String MAGISK_ALT_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Alt-Repo";
private static final Object lock = new Object();
private static RepoManager INSTANCE;
public static RepoManager getINSTANCE() {
if (INSTANCE == null) {
synchronized (lock) {
if (INSTANCE == null) {
MainApplication mainApplication = MainApplication.getINSTANCE();
if (mainApplication != null) {
INSTANCE = new RepoManager(mainApplication);
} else {
throw new RuntimeException("Getting RepoManager too soon!");
}
}
}
}
return INSTANCE;
}
private final MainApplication mainApplication;
private final LinkedHashMap<String, RepoData> repoData;
private final HashMap<String, RepoModule> modules;
private RepoManager(MainApplication mainApplication) {
this.mainApplication = mainApplication;
this.repoData = new LinkedHashMap<>();
this.modules = new HashMap<>();
// We do not have repo list config yet.
this.addRepoData(MAGISK_REPO);
this.addRepoData(MAGISK_ALT_REPO);
// Populate default cache
for (RepoData repoData:this.repoData.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 (repoModule.moduleInfo.versionCode >
registeredRepoModule.moduleInfo.versionCode) {
this.modules.put(repoModule.id, repoModule);
}
} else {
Log.e(TAG, "Detected module with invalid metadata: " + repoModule.id);
}
}
}
}
public RepoData get(String url) {
return this.repoData.get(url);
}
public RepoData addOrGet(String url) {
RepoData repoData;
synchronized (this.repoUpdateLock) {
repoData = this.repoData.get(url);
if (repoData == null) {
return this.addRepoData(url);
}
}
return repoData;
}
public interface UpdateListener {
void update(double value);
}
private final Object repoUpdateLock = new Object();
private boolean repoUpdating;
private boolean repoLastResult = false;
public boolean isRepoUpdating() {
return this.repoUpdating;
}
public final void afterUpdate() {
if (this.repoUpdating) synchronized (this.repoUpdateLock) {}
}
public final void runAfterUpdate(Runnable runnable) {
synchronized (this.repoUpdateLock) {
runnable.run();
}
}
// MultiThread friendly method
public final void update(UpdateListener updateListener) {
if (!this.repoUpdating) {
// Do scan
synchronized (this.repoUpdateLock) {
this.repoUpdating = true;
try {
this.repoLastResult =
this.scanInternal(updateListener);
} finally {
this.repoUpdating = false;
}
}
} else {
// Wait for current scan
synchronized (this.repoUpdateLock) {}
}
}
private static final double STEP1 = 0.1D;
private static final double STEP2 = 0.8D;
private static final double STEP3 = 0.1D;
private boolean scanInternal(UpdateListener updateListener) {
this.modules.clear();
updateListener.update(0D);
RepoData[] repoDatas = this.repoData.values().toArray(new RepoData[0]);
RepoUpdater[] repoUpdaters = new RepoUpdater[repoDatas.length];
int moduleToUpdate = 0;
for (int i = 0; i < repoDatas.length; i++) {
moduleToUpdate += (repoUpdaters[i] =
new RepoUpdater(repoDatas[i])).fetchIndex();
updateListener.update(STEP1 / repoDatas.length * (i + 1));
}
int updatedModules = 0;
for (int i = 0; i < repoUpdaters.length; i++) {
List<RepoModule> repoModules = repoUpdaters[i].toUpdate();
RepoData repoData = repoDatas[i];
for (RepoModule repoModule:repoModules) {
try {
Files.write(new File(repoData.cacheRoot, repoModule.id + ".prop"),
Http.doHttpGet(repoModule.propUrl, false));
if (repoDatas[i].tryLoadMetadata(repoModule)) {
// Note: registeredRepoModule may not null if registered by multiple repos
RepoModule registeredRepoModule = this.modules.get(repoModule.id);
if (registeredRepoModule == null) {
this.modules.put(repoModule.id, repoModule);
} else if (repoModule.moduleInfo.versionCode >
registeredRepoModule.moduleInfo.versionCode) {
this.modules.put(repoModule.id, repoModule);
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to get \"" + repoModule.id + "\" metadata", e);
}
updatedModules++;
updateListener.update(STEP1 + (STEP2 / moduleToUpdate * updatedModules));
}
for (RepoModule repoModule:repoUpdaters[i].toApply()) {
if ((repoModule.moduleInfo.flags & ModuleInfo.FLAG_METADATA_INVALID) == 0) {
RepoModule registeredRepoModule = this.modules.get(repoModule.id);
if (registeredRepoModule == null) {
this.modules.put(repoModule.id, repoModule);
} else if (repoModule.moduleInfo.versionCode >
registeredRepoModule.moduleInfo.versionCode) {
this.modules.put(repoModule.id, repoModule);
}
}
}
}
boolean hasInternet = false;
for (int i = 0; i < repoDatas.length; i++) {
hasInternet |= repoUpdaters[i].finish();
updateListener.update(STEP1 + STEP2 + (STEP3 / repoDatas.length * (i + 1)));
}
Log.i(TAG, "Got " + this.modules.size() + " modules!");
updateListener.update(1D);
return hasInternet;
}
public HashMap<String, RepoModule> getModules() {
this.afterUpdate();
return this.modules;
}
public boolean hasConnectivity() {
return this.repoLastResult;
}
public static String internalIdOfUrl(String url) {
switch (url) {
case MAGISK_REPO_MANAGER:
case MAGISK_REPO:
return "magisk_repo";
case MAGISK_ALT_REPO:
return "magisk_alt_repo";
default:
return "repo_" + Hashes.hashSha1(url);
}
}
private RepoData addRepoData(String url) {
String id = internalIdOfUrl(url);
File cacheRoot = new File(this.mainApplication.getCacheDir(), id);
SharedPreferences sharedPreferences = this.mainApplication
.getSharedPreferences("mmm_" + id, Context.MODE_PRIVATE);
RepoData repoData = new RepoData(url, cacheRoot, sharedPreferences);
this.repoData.put(url, repoData);
return repoData;
}
}

@ -0,0 +1,20 @@
package com.fox2code.mmm.repo;
import com.fox2code.mmm.manager.ModuleInfo;
public class RepoModule {
public final ModuleInfo moduleInfo;
public final String id;
public long lastUpdated;
public String propUrl;
public String zipUrl;
public String notesUrl;
boolean processed;
public RepoModule(String id) {
this.moduleInfo = new ModuleInfo(id);
this.id = id;
this.moduleInfo.flags |=
ModuleInfo.FLAG_METADATA_INVALID;
}
}

@ -0,0 +1,69 @@
package com.fox2code.mmm.repo;
import android.util.Log;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class RepoUpdater {
private static final String TAG = "RepoUpdater";
public final RepoData repoData;
public byte[] indexRaw;
private List<RepoModule> toUpdate;
private Set<RepoModule> toApply;
public RepoUpdater(RepoData repoData) {
this.repoData = repoData;
}
public int fetchIndex() {
try {
this.indexRaw = Http.doHttpGet(this.repoData.url, false);
this.toUpdate = this.repoData.populate(new JSONObject(
new String(this.indexRaw, StandardCharsets.UTF_8)));
// Since we reuse instances this should work
this.toApply = new HashSet<>(this.repoData.moduleHashMap.values());
this.toApply.removeAll(this.toUpdate);
// Return repo to update
return this.toUpdate.size();
} catch (Exception e) {
Log.e(TAG, "Failed to get manifest", e);
this.indexRaw = null;
this.toUpdate = Collections.emptyList();
this.toApply = Collections.emptySet();
return 0;
}
}
public List<RepoModule> toUpdate() {
return this.toUpdate;
}
public Set<RepoModule> toApply() {
return this.toApply;
}
public boolean finish() {
final boolean success = this.indexRaw != null;
if (this.indexRaw != null) {
try {
Files.write(this.repoData.metaDataCache, this.indexRaw);
} catch (IOException e) {
e.printStackTrace();
}
this.indexRaw = null;
}
this.toUpdate = null;
this.toApply = null;
return success;
}
}

@ -0,0 +1,106 @@
package com.fox2code.mmm.settings;
import android.os.Bundle;
import androidx.annotation.StyleRes;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.repo.RepoData;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.utils.IntentHelper;
import com.mikepenz.aboutlibraries.LibsBuilder;
public class SettingsActivity extends CompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.settings_activity);
if (savedInstanceState == null) {
setTitle(R.string.app_name);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings, new SettingsFragment())
.commit();
}
}
public static class SettingsFragment extends PreferenceFragmentCompat
implements CompatActivity.OnBackPressedCallback {
@Override
@SuppressWarnings("ConstantConditions")
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
getPreferenceManager().setSharedPreferencesName("mmm");
setPreferencesFromResource(R.xml.root_preferences, rootKey);
findPreference("pref_theme").setOnPreferenceChangeListener((preference, newValue) -> {
@StyleRes int themeResId;
switch (String.valueOf(newValue)) {
default:
case "system":
themeResId = R.style.Theme_MagiskModuleManager;
break;
case "dark":
themeResId = R.style.Theme_MagiskModuleManager_Dark;
break;
case "light":
themeResId = R.style.Theme_MagiskModuleManager_Light;
break;
}
MainApplication.getINSTANCE().setManagerThemeResId(themeResId);
CompatActivity.getCompatActivity(this).setThemeRecreate(themeResId);
return true;
});
setRepoNameResolution("pref_repo_main", RepoManager.MAGISK_REPO,
"Magisk Modules Repo (Official)", RepoManager.MAGISK_REPO_HOMEPAGE);
setRepoNameResolution("pref_repo_alt", RepoManager.MAGISK_ALT_REPO,
"Magisk Modules Alt Repo", RepoManager.MAGISK_ALT_REPO_HOMEPAGE);
final LibsBuilder libsBuilder = new LibsBuilder()
.withFields(R.string.class.getFields()).withShowLoadingProgress(false)
.withLicenseShown(true).withAboutMinimalDesign(false);
findPreference("pref_source_code").setOnPreferenceClickListener(p -> {
IntentHelper.openUrl(p.getContext(), "https://github.com/Fox2Code/FoxMagiskModuleManager");
return true;
});
findPreference("pref_show_licenses").setOnPreferenceClickListener(p -> {
CompatActivity compatActivity = getCompatActivity(this);
compatActivity.setOnBackPressedCallback(this);
compatActivity.setTitle(R.string.licenses);
compatActivity.getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings, libsBuilder.supportFragment())
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit();
return true;
});
}
private void setRepoNameResolution(String preferenceName,String url,
String fallbackTitle,String homepage) {
Preference preference = findPreference(preferenceName);
if (preference == null) return;
RepoData repoData = RepoManager.getINSTANCE().get(url);
preference.setTitle(repoData == null ? fallbackTitle :
repoData.getNameOrFallback(fallbackTitle));
preference.setOnPreferenceClickListener(p -> {
IntentHelper.openUrl(getCompatActivity(this), homepage);
return true;
});
}
@Override
public boolean onBackPressed(CompatActivity compatActivity) {
compatActivity.setTitle(R.string.app_name);
compatActivity.getSupportFragmentManager()
.beginTransaction().replace(R.id.settings, this)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit();
return true;
}
}
}

@ -0,0 +1,105 @@
package com.fox2code.mmm.utils;
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 {
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 {
try (InputStream inputStream = SuFileInputStream.open(file)) {
return readAllBytes(inputStream);
}
}
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 byte[] readAllBytes(InputStream inputStream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
copy(inputStream, buffer);
return buffer.toByteArray();
}
public static byte[] patchModuleSimple(byte[] bytes) throws IOException {
ByteArrayOutputStream byteArrayOutputStream =
new ByteArrayOutputStream((int) (bytes.length * 1.2F));
patchModuleSimple(bytes, byteArrayOutputStream);
return byteArrayOutputStream.toByteArray();
}
public static void patchModuleSimple(byte[] bytes,OutputStream outputStream) throws IOException {
if (bytes[0x6] == 0x0 && bytes[0x7] == 0x0 && bytes[0x8] == 0x8) bytes[0x7] = 0x8;
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();
}
}

@ -0,0 +1,60 @@
/*
* Copyright (c) 2021 Fox2Code
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* 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;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;
/** Open implementation of ProviderInstaller.installIfNeeded
* (Compatible with MicroG even without signature spoofing)
*/
// Note: This code is MIT because I took it from another unpublished project I had
// I might upstream this to MicroG at some point
public class GMSProviderInstaller {
private static final String TAG = "GMSProviderInstaller";
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;
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");
cl.getDeclaredMethod("insertProvider", Context.class).invoke(null, remote);
Log.i(TAG, "Installed GMS security providers!");
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "No GMS Implementation are installed on this device");
} catch (Exception e) {
Log.w(TAG, "Failed to install the provider of the current GMS Implementation", e);
}
}
}

@ -0,0 +1,28 @@
package com.fox2code.mmm.utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Hashes {
private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
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 hashSha1(String input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
return bytesToHex(md.digest(input.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}

@ -0,0 +1,187 @@
package com.fox2code.mmm.utils;
import android.util.Log;
import androidx.annotation.NonNull;
import com.fox2code.mmm.MainApplication;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import okhttp3.Cache;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.dnsoverhttps.DnsOverHttps;
public class Http {
private static final OkHttpClient httpClient;
private static final OkHttpClient httpClientCachable;
static {
OkHttpClient.Builder httpclientBuilder = new OkHttpClient.Builder();
httpclientBuilder.connectTimeout(11, TimeUnit.SECONDS);
try {
httpclientBuilder.dns(new DnsOverHttps.Builder().client(httpclientBuilder.build()).url(
Objects.requireNonNull(HttpUrl.parse("https://cloudflare-dns.com/dns-query")))
.bootstrapDnsHosts(
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")
).resolvePrivateAddresses(true).build());
} catch (UnknownHostException e) {
e.printStackTrace();
}
httpClient = httpclientBuilder.build();
MainApplication mainApplication = MainApplication.getINSTANCE();
if (mainApplication != null) {
httpclientBuilder.cache(new Cache(
new File(mainApplication.getCacheDir(), "http_cache"),
1024L * 1024L)); // 1Mo of cache
httpclientBuilder.cookieJar(new CDNCookieJar());
httpClientCachable = httpclientBuilder.build();
} else {
httpClientCachable = httpClient;
}
}
public static OkHttpClient getHttpclientNoCache() {
return httpClient;
}
public static OkHttpClient getHttpclientWithCache() {
return httpClientCachable;
}
private static Request.Builder makeRequestBuilder() {
return new Request.Builder().header("Connection", "keep-alive")
.header("Upgrade-Insecure-Requests", "1");
}
public static byte[] doHttpGet(String url,boolean allowCache) throws IOException {
Response response = (allowCache ? httpClientCachable : httpClient).newCall(
makeRequestBuilder().url(url).get().build()
).execute();
// 200 == success, 304 == cache valid
if (response.code() != 200 && (response.code() != 304 || !allowCache)) {
throw new IOException("Received error code: "+ response.code());
}
ResponseBody responseBody = response.body();
// Use cache api if used cached response
if (responseBody == null && response.code() == 304) {
response = response.cacheResponse();
if (response != null)
responseBody = response.body();
}
return responseBody == null ? new byte[0] : responseBody.bytes();
}
public static byte[] doHttpGet(String url,ProgressListener progressListener) throws IOException {
Log.d("Http", "Progress URL: " + url);
Response response = httpClient.newCall(makeRequestBuilder().url(url).get().build()).execute();
if (response.code() != 200) {
throw new IOException("Received error code: "+ response.code());
}
ResponseBody responseBody = Objects.requireNonNull(response.body());
InputStream inputStream = responseBody.byteStream();
byte[] buff = new byte[1024 * 4];
long downloaded = 0;
long target = responseBody.contentLength();
ByteArrayOutputStream byteArrayOutputStream =
new ByteArrayOutputStream();
int divider = 1; // Make everything go in an int
while ((target / divider) > (Integer.MAX_VALUE / 2)) {
divider *= 2;
}
final long UPDATE_INTERVAL = 100;
long nextUpdate = System.currentTimeMillis() + UPDATE_INTERVAL;
long currentUpdate;
Log.d("Http", "Target: " + target + " Divider: " + divider);
progressListener.onUpdate(0, (int) (target / divider), false);
while (true) {
int read = inputStream.read(buff);
if(read == -1) break;
byteArrayOutputStream.write(buff, 0, read);
downloaded += read;
currentUpdate = System.currentTimeMillis();
if (nextUpdate < currentUpdate) {
nextUpdate = currentUpdate + UPDATE_INTERVAL;
progressListener.onUpdate((int) (downloaded / divider), (int) (target / divider), false);
}
}
inputStream.close();
progressListener.onUpdate((int) (downloaded / divider), (int) (target / divider), true);
return byteArrayOutputStream.toByteArray();
}
/**
* 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 help CDN and cache to work together.
* */
private static class CDNCookieJar implements CookieJar {
private final HashMap<String, Cookie> cookieMap = new HashMap<>();
@NonNull
@Override
public List<Cookie> loadForRequest(@NonNull HttpUrl httpUrl) {
if (!httpUrl.isHttps()) return Collections.emptyList();
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;
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);
}
}
}
public interface ProgressListener {
void onUpdate(int downloaded,int total, boolean done);
}
}

@ -0,0 +1,193 @@
package com.fox2code.mmm.utils;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.core.app.ActivityOptionsCompat;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.installer.InstallerActivity;
import com.fox2code.mmm.markdown.MarkdownActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class IntentHelper {
public static void openUrl(Context context, String url) {
try {
Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
context.startActivity(myIntent);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, "No application can handle this request."
+ " Please install a webbrowser", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
public static String getPackageOfConfig(String config) {
int i = config.indexOf(' ');
if (i != -1)
config = config.substring(0, i);
i = config.indexOf('/');
if (i != -1)
config = config.substring(0, i);
return config;
}
public static void openConfig(Context context, String config) {
String pkg = getPackageOfConfig(config);
try {
Intent intent = context.getPackageManager()
.getLaunchIntentForPackage(pkg);
if (intent == null) {
intent = new Intent("android.intent.action.APPLICATION_PREFERENCES");
intent.setPackage(pkg);
}
intent.putExtra(Constants.EXTRA_FROM_MANAGER, true);
startActivity(context, intent, false);
} catch (ActivityNotFoundException e) {
Toast.makeText(context,
"Failed to launch module config activity", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
public static void openMarkdown(Context context, String url, String title, String config) {
try {
Intent intent = new Intent(context, MarkdownActivity.class);
MainApplication.addSecret(intent);
intent.putExtra(Constants.EXTRA_MARKDOWN_URL, url);
intent.putExtra(Constants.EXTRA_MARKDOWN_TITLE, title);
if (config != null && !config.isEmpty())
intent.putExtra(Constants.EXTRA_MARKDOWN_CONFIG, config);
startActivity(context, intent, true);
} 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) {
try {
Intent intent = new Intent(context, InstallerActivity.class);
intent.setAction(Constants.INTENT_INSTALL_INTERNAL);
MainApplication.addSecret(intent);
intent.putExtra(Constants.EXTRA_INSTALL_PATH, url);
intent.putExtra(Constants.EXTRA_INSTALL_NAME, title);
if (config != null && !config.isEmpty())
intent.putExtra(Constants.EXTRA_INSTALL_CONFIG, config);
startActivity(context, intent, true);
} 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);
}
public static void startActivity(Context context, Intent intent,boolean sameApp)
throws ActivityNotFoundException {
int flags = intent.getFlags();
if (sameApp) {
flags &= ~Intent.FLAG_ACTIVITY_NEW_TASK;
// flags |= Intent.FLAG_ACTIVITY_REORDER_TO_FRONT;
} else {
flags &= ~Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
flags |= Intent.FLAG_ACTIVITY_NEW_TASK;
}
intent.setFlags(flags);
Activity activity = getActivity(context);
Bundle param = ActivityOptionsCompat.makeCustomAnimation(context,
android.R.anim.fade_in, android.R.anim.fade_out).toBundle();
if (activity == null) {
context.startActivity(intent, param);
} else {
if (sameApp) {
intent.putExtra(Constants.EXTRA_FADE_OUT, true);
activity.overridePendingTransition(
android.R.anim.fade_in, android.R.anim.fade_out);
}
activity.startActivity(intent, param);
}
}
public static Activity getActivity(Context context) {
while (!(context instanceof Activity)) {
if (context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
} else return null;
}
return (Activity) context;
}
public static void openFileTo(CompatActivity compatActivity, File destination,
OnFileReceivedCallback callback) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT).setType("application/zip");
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
intent.addCategory(Intent.CATEGORY_OPENABLE);
Bundle param = ActivityOptionsCompat.makeCustomAnimation(compatActivity,
android.R.anim.fade_in, android.R.anim.fade_out).toBundle();
compatActivity.startActivityForResult(intent, param, (result, data) -> {
String name = destination.getName();
if (data == null || result != Activity.RESULT_OK) {
callback.onReceived(destination, false);
return;
}
Uri uri = data.getData();
if (uri == null || "http".equals(uri.getScheme()) ||
"https".equals(uri.getScheme())) {
callback.onReceived(destination, false);
return;
}
InputStream inputStream = null;
OutputStream outputStream = null;
boolean success = false;
try {
inputStream = compatActivity.getContentResolver()
.openInputStream(uri);
outputStream = new FileOutputStream(destination);
Files.copy(inputStream, outputStream);
String newName = uri.getLastPathSegment();
if (newName.endsWith(".zip")) name = newName;
success = true;
} catch (Exception e) {
Log.e("IntentHelper", "fail copy", e);
} finally {
Files.closeSilently(inputStream);
Files.closeSilently(outputStream);
if (!success && destination.exists() && !destination.delete())
Log.e("IntentHelper", "Failed to delete artefact!");
}
callback.onReceived(destination, success);
});
}
public interface OnFileReceivedCallback {
void onReceived(File target,boolean success);
}
}

@ -0,0 +1,137 @@
package com.fox2code.mmm.utils;
import android.os.Build;
import com.fox2code.mmm.manager.ModuleInfo;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
public class PropUtils {
private static final HashMap<String, String> moduleConfigsFallbacks = new HashMap<>();
private static final HashMap<String, Integer> moduleMinApiFallbacks = new HashMap<>();
private static final int RIRU_MIN_API;
// Note: These fallback values may not be up-to-date
// They are only used if modules don't define the metadata
static {
// Config are application installed by modules that allow them to be configured
moduleConfigsFallbacks.put("quickstepswitcher", "xyz.paphonb.quickstepswitcher");
moduleConfigsFallbacks.put("riru_edxposed", "org.meowcat.edxposed.manager");
moduleConfigsFallbacks.put("riru_lsposed", "org.lsposed.manager");
moduleConfigsFallbacks.put("xposed_dalvik", "de.robv.android.xposed.installer");
moduleConfigsFallbacks.put("xposed", "de.robv.android.xposed.installer");
moduleConfigsFallbacks.put("substratum", "projekt.substratum");
// minApi is the minimum android version required to use the module
moduleMinApiFallbacks.put("riru_ifw_enhance", Build.VERSION_CODES.O);
moduleMinApiFallbacks.put("riru_edxposed", Build.VERSION_CODES.O);
moduleMinApiFallbacks.put("riru_lsposed", Build.VERSION_CODES.O_MR1);
moduleMinApiFallbacks.put("noneDisplayCutout", Build.VERSION_CODES.P);
moduleMinApiFallbacks.put("quickstepswitcher", Build.VERSION_CODES.P);
moduleMinApiFallbacks.put("riru_clipboard_whitelist", Build.VERSION_CODES.Q);
// minApi for riru core include submodules
moduleMinApiFallbacks.put("riru-core", RIRU_MIN_API = Build.VERSION_CODES.M);
}
public static void readProperties(ModuleInfo moduleInfo, String file) throws IOException {
boolean readId = false, readVersionCode = false;
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(SuFileInputStream.open(file), StandardCharsets.UTF_8))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
int index = line.indexOf('=');
if (index == -1 || line.startsWith("#"))
continue;
String key = line.substring(0, index);
String value = line.substring(index + 1).trim();
switch (key) {
case "id":
readId = true;
if (!moduleInfo.id.equals(value)) {
throw new IOException(file + " has an non matching module id! "+
"(Expected \"" + moduleInfo.id + "\" got \"" + value + "\"");
}
break;
case "name":
moduleInfo.name = value;
break;
case "version":
moduleInfo.version = value;
break;
case "versionCode":
readVersionCode = true;
moduleInfo.versionCode = Integer.parseInt(value);
break;
case "author":
moduleInfo.author = value;
break;
case "description":
moduleInfo.description = value;
break;
case "support":
// Do not accept invalid or too broad support links
if (!value.startsWith("https://") ||
"https://forum.xda-developers.com/".equals(value))
break;
moduleInfo.support = value;
break;
case "donate":
// Do not accept invalid donate links
if (!value.startsWith("https://")) break;
moduleInfo.donate = value;
break;
case "config":
moduleInfo.config = value;
break;
case "minMagisk":
try {
moduleInfo.minMagisk = Integer.parseInt(value);
} catch (Exception e) {
moduleInfo.minMagisk = 0;
}
break;
case "minApi":
// Special case for Riru EdXposed because
// minApi don't mean the same thing for them
if (moduleInfo.id.equals("riru_edxposed") &&
"10".equals(value)) {
break;
}
try {
moduleInfo.minApi = Integer.parseInt(value);
} catch (Exception e) {
moduleInfo.minApi = 0;
}
break;
}
}
}
if (!readId) {
throw new IOException("Didn't read module id at least once!");
}
if (!readVersionCode) {
throw new IOException("Didn't read module versionCode at least once!");
}
if (moduleInfo.name == null) {
moduleInfo.name = moduleInfo.id;
}
if (moduleInfo.version == null) {
moduleInfo.version = "v" + moduleInfo.versionCode;
}
if (moduleInfo.minApi == 0) {
Integer minApiFallback = moduleMinApiFallbacks.get(moduleInfo.id);
if (minApiFallback != null)
moduleInfo.minApi = minApiFallback;
else if (moduleInfo.id.startsWith("riru_")
|| moduleInfo.id.startsWith("riru-"))
moduleInfo.minApi = RIRU_MIN_API;
}
if (moduleInfo.config == null) {
moduleInfo.config = moduleConfigsFallbacks.get(moduleInfo.id);
}
}
}

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

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4c-1.48,0 -2.85,0.43 -4.01,1.17l1.46,1.46C10.21,6.23 11.08,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3 0,1.13 -0.64,2.11 -1.56,2.62l1.45,1.45C23.16,18.16 24,16.68 24,15c0,-2.64 -2.05,-4.78 -4.65,-4.96zM3,5.27l2.75,2.74C2.56,8.15 0,10.77 0,14c0,3.31 2.69,6 6,6h11.73l2,2L21,20.73 4.27,4 3,5.27zM7.73,10l8,8H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h1.73z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<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.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>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<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,9h8v10L8,19L8,9zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
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.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>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13.41,18.09L13.41,20h-2.67v-1.93c-1.71,-0.36 -3.16,-1.46 -3.27,-3.4h1.96c0.1,1.05 0.82,1.87 2.65,1.87 1.96,0 2.4,-0.98 2.4,-1.59 0,-0.83 -0.44,-1.61 -2.67,-2.14 -2.48,-0.6 -4.18,-1.62 -4.18,-3.67 0,-1.72 1.39,-2.84 3.11,-3.21L10.74,4h2.67v1.95c1.86,0.45 2.79,1.86 2.85,3.39L14.3,9.34c-0.05,-1.11 -0.64,-1.87 -2.22,-1.87 -1.5,0 -2.4,0.68 -2.4,1.64 0,0.84 0.65,1.39 2.67,1.91s4.18,1.39 4.18,3.91c-0.01,1.83 -1.38,2.83 -3.12,3.16z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,3L4,3c-1.1,0 -2,0.9 -2,2v11c0,1.1 0.9,2 2,2h3l-1,1v2h12v-2l-1,-1h3c1.1,0 2,-0.9 2,-2L22,5c0,-1.1 -0.9,-2 -2,-2zM20,16L4,16L4,5h16v11z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20.5,10L21,8h-4l1,-4h-2l-1,4h-4l1,-4h-2L9,8H5l-0.5,2h4l-1,4h-4L3,16h4l-1,4h2l1,-4h4l-1,4h2l1,-4h4l0.5,-2h-4l1,-4H20.5zM13.5,14h-4l1,-4h4L13.5,14z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10c1.38,0 2.5,-1.12 2.5,-2.5c0,-0.61 -0.23,-1.2 -0.64,-1.67c-0.08,-0.1 -0.13,-0.21 -0.13,-0.33c0,-0.28 0.22,-0.5 0.5,-0.5H16c3.31,0 6,-2.69 6,-6C22,6.04 17.51,2 12,2zM17.5,13c-0.83,0 -1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5C19,12.33 18.33,13 17.5,13zM14.5,9C13.67,9 13,8.33 13,7.5C13,6.67 13.67,6 14.5,6S16,6.67 16,7.5C16,8.33 15.33,9 14.5,9zM5,11.5C5,10.67 5.67,10 6.5,10S8,10.67 8,11.5C8,12.33 7.33,13 6.5,13S5,12.33 5,11.5zM11,7.5C11,8.33 10.33,9 9.5,9S8,8.33 8,7.5C8,6.67 8.67,6 9.5,6S11,6.67 11,7.5z"/>
</vector>

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9.93,12.99c0.1,0 2.42,0.1 3.8,-0.24c0,0 0.01,0 0.01,0c1.59,-0.39 3.8,-1.51 4.37,-5.17c0,0 1.27,-4.58 -5.03,-4.58H7.67C7.18,3 6.76,3.36 6.68,3.84l-2.3,14.56c-0.05,0.3 0.19,0.58 0.49,0.58H8.3h0l0.84,-5.32C9.2,13.28 9.53,12.99 9.93,12.99z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M18.99,8.29c-0.81,3.73 -3.36,5.7 -7.42,5.7H10.1l-1.03,6.52C9.03,20.77 9.23,21 9.49,21h1.9c0.34,0 0.64,-0.25 0.69,-0.59c0.08,-0.4 0.52,-3.32 0.61,-3.82c0.05,-0.34 0.35,-0.59 0.69,-0.59h0.44c2.82,0 5.03,-1.15 5.68,-4.46C19.76,10.2 19.62,9.1 18.99,8.29z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M2,20h20v-4L2,16v4zM4,17h2v2L4,19v-2zM2,4v4h20L22,4L2,4zM6,7L4,7L4,5h2v2zM2,14h20v-4L2,10v4zM4,11h2v2L4,13v-2z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10s10,-4.48 10,-10C22,6.48 17.52,2 12,2zM19.46,9.12l-2.78,1.15c-0.51,-1.36 -1.58,-2.44 -2.95,-2.94l1.15,-2.78C16.98,5.35 18.65,7.02 19.46,9.12zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3s3,1.34 3,3S13.66,15 12,15zM9.13,4.54l1.17,2.78c-1.38,0.5 -2.47,1.59 -2.98,2.97L4.54,9.13C5.35,7.02 7.02,5.35 9.13,4.54zM4.54,14.87l2.78,-1.15c0.51,1.38 1.59,2.46 2.97,2.96l-1.17,2.78C7.02,18.65 5.35,16.98 4.54,14.87zM14.88,19.46l-1.15,-2.78c1.37,-0.51 2.45,-1.59 2.95,-2.97l2.78,1.17C18.65,16.98 16.98,18.65 14.88,19.46z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2zM16.64,8.8c-0.15,1.58 -0.8,5.42 -1.13,7.19c-0.14,0.75 -0.42,1 -0.68,1.03c-0.58,0.05 -1.02,-0.38 -1.58,-0.75c-0.88,-0.58 -1.38,-0.94 -2.23,-1.5c-0.99,-0.65 -0.35,-1.01 0.22,-1.59c0.15,-0.15 2.71,-2.48 2.76,-2.69c0.01,-0.03 0.01,-0.12 -0.05,-0.18c-0.06,-0.05 -0.14,-0.03 -0.21,-0.02c-0.09,0.02 -1.49,0.95 -4.22,2.79c-0.4,0.27 -0.76,0.41 -1.08,0.4c-0.36,-0.01 -1.04,-0.2 -1.55,-0.37c-0.63,-0.2 -1.12,-0.31 -1.08,-0.66c0.02,-0.18 0.27,-0.36 0.74,-0.55c2.92,-1.27 4.86,-2.11 5.83,-2.51c2.78,-1.16 3.35,-1.36 3.73,-1.36c0.08,0 0.27,0.02 0.39,0.12c0.1,0.08 0.13,0.19 0.14,0.27C16.63,8.48 16.65,8.66 16.64,8.8z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1c-2.73,2.71 -2.73,7.08 0,9.79s7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29c-3.51,3.48 -9.21,3.48 -12.72,0c-3.5,-3.47 -3.53,-9.11 -0.02,-12.58s9.14,-3.47 12.65,0L21,3V10.12zM12.5,8v4.25l3.5,2.08l-0.72,1.21L11,13V8H12.5z"/>
</vector>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M11.9991,2C6.4771,2 2,6.4771 2,12.0003C2,16.4185 4.865,20.1663 8.8388,21.4892C9.3391,21.5807 9.5214,21.2719 9.5214,21.0067C9.5214,20.7698 9.5128,20.1405 9.5079,19.3062C6.7264,19.9103 6.1395,17.9655 6.1395,17.9655C5.6846,16.8102 5.0289,16.5026 5.0289,16.5026C4.121,15.8826 5.0977,15.8948 5.0977,15.8948C6.1014,15.9654 6.6294,16.9256 6.6294,16.9256C7.5213,18.4535 8.9701,18.0122 9.5398,17.7562C9.6307,17.1103 9.8885,16.6696 10.1746,16.4197C7.9541,16.1674 5.6195,15.3092 5.6195,11.4773C5.6195,10.3858 6.0093,9.4932 6.649,8.7939C6.5459,8.541 6.2027,7.5244 6.7466,6.1474C6.7466,6.1474 7.5864,5.8786 9.4969,7.1726C10.2943,6.9504 11.1501,6.8399 12.0003,6.8362C12.8493,6.8399 13.7051,6.9504 14.5038,7.1726C16.413,5.8786 17.2509,6.1474 17.2509,6.1474C17.7967,7.5244 17.4535,8.541 17.3504,8.7939C17.9913,9.4932 18.3786,10.3858 18.3786,11.4773C18.3786,15.319 16.0403,16.1643 13.8125,16.4117C14.1716,16.7205 14.4915,17.3307 14.4915,18.2638C14.4915,19.6003 14.4792,20.6789 14.4792,21.0067C14.4792,21.2744 14.6591,21.5856 15.1668,21.488C19.1374,20.1626 22,16.4173 22,12.0003C22,6.4771 17.5223,2 11.9991,2Z" />
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M15.386,0.524c-4.764,0 -8.64,3.876 -8.64,8.64 0,4.75 3.876,8.613 8.64,8.613 4.75,0 8.614,-3.864 8.614,-8.613C24,4.4 20.136,0.524 15.386,0.524M0.003,23.537h4.22V0.524H0.003"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M-0.05,16.79L3.19,12.97L-0.05,9.15L1.5,7.86L4.5,11.41L7.5,7.86L9.05,9.15L5.81,12.97L9.05,16.79L7.5,18.07L4.5,14.5L1.5,18.07L-0.05,16.79M24,17A1,1 0 0,1 23,18H20A2,2 0 0,1 18,16V14A2,2 0 0,1 20,12H22V10H18V8H23A1,1 0 0,1 24,9M22,14H20V16H22V14M16,17A1,1 0 0,1 15,18H12A2,2 0 0,1 10,16V10A2,2 0 0,1 12,8H14V5H16V17M14,16V10H12V16H14Z"/>
</vector>

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/module_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_bar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:indeterminate="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/search_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:gravity="center_vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.cardview.widget.CardView
android:id="@+id/search_card"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
app:cardCornerRadius="@dimen/card_corner_radius"
app:strokeColor="@android:color/transparent"
app:strokeWidth="0dp">
<androidx.appcompat.widget.SearchView
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.cardview.widget.CardView>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<HorizontalScrollView
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/install_terminal"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textSize="16sp" />
</HorizontalScrollView>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_bar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:indeterminate="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:id="@+id/markdownBackground">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/markdown_border_content"
android:gravity="center_vertical"
app:cardCornerRadius="@dimen/card_corner_radius" >
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/markdownView"
android:text="@string/loading"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</androidx.cardview.widget.CardView>
</LinearLayout>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/markdownView"
android:text="@string/loading"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:gravity="center_vertical">
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="@dimen/card_corner_radius"
app:strokeColor="@android:color/transparent"
app:strokeWidth="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@null">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/button_action"
android:textSize="16sp"
android:src="@drawable/ic_baseline_delete_forever_24"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:importantForAccessibility="no"
android:layout_marginLeft="8dp"
tools:ignore="RtlHardcoded" />
<!-- Module components -->
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<TextView
android:id="@+id/title_text"
android:textSize="16sp"
android:maxLines="1"
android:text="@string/loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/button_action" />
<TextView
android:id="@+id/credit_text"
android:textSize="12sp"
android:text="@string/loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/title_text"
app:layout_constraintTop_toBottomOf="@id/title_text" />
<TextView
android:id="@+id/description_text"
android:textSize="16sp"
android:text="@string/loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_below="@+id/credit_text"
app:layout_constraintTop_toBottomOf="@id/credit_text" />
<TextView
android:id="@+id/updated_text"
android:textSize="12sp"
android:text="@string/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="bottom"
app:layout_constraintTop_toBottomOf="@+id/description_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
<!-- Module actions -->
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/button_action1"
android:textSize="16sp"
android:visibility="gone"
android:src="@drawable/ic_baseline_error_24"
android:layout_width="@dimen/module_action_icon_size"
android:layout_height="@dimen/module_action_icon_size"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintTop_toBottomOf="@id/description_text"
app:layout_constraintRight_toRightOf="parent"
android:importantForAccessibility="no"
android:layout_marginLeft="8dp"
android:layout_marginRight="3dp"
android:layout_marginBottom="4dp"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/button_action2"
android:textSize="16sp"
android:visibility="gone"
android:src="@drawable/ic_baseline_error_24"
android:layout_width="@dimen/module_action_icon_size"
android:layout_height="@dimen/module_action_icon_size"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintTop_toBottomOf="@id/description_text"
app:layout_constraintRight_toLeftOf="@id/button_action1"
android:importantForAccessibility="no"
android:layout_marginLeft="8dp"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/button_action3"
android:textSize="16sp"
android:visibility="gone"
android:src="@drawable/ic_baseline_error_24"
android:layout_width="@dimen/module_action_icon_size"
android:layout_height="@dimen/module_action_icon_size"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintTop_toBottomOf="@id/description_text"
app:layout_constraintRight_toLeftOf="@id/button_action2"
android:importantForAccessibility="no"
android:layout_marginLeft="8dp"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/button_action4"
android:textSize="16sp"
android:visibility="gone"
android:src="@drawable/ic_baseline_error_24"
android:layout_width="@dimen/module_action_icon_size"
android:layout_height="@dimen/module_action_icon_size"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintTop_toBottomOf="@id/description_text"
app:layout_constraintRight_toLeftOf="@id/button_action3"
android:importantForAccessibility="no"
android:layout_marginLeft="8dp"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/button_action5"
android:textSize="16sp"
android:visibility="gone"
android:src="@drawable/ic_baseline_error_24"
android:layout_width="@dimen/module_action_icon_size"
android:layout_height="@dimen/module_action_icon_size"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintTop_toBottomOf="@id/description_text"
app:layout_constraintRight_toLeftOf="@id/button_action4"
android:importantForAccessibility="no"
android:layout_marginLeft="8dp"
tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/button_action6"
android:textSize="16sp"
android:visibility="gone"
android:src="@drawable/ic_baseline_error_24"
android:layout_width="@dimen/module_action_icon_size"
android:layout_height="@dimen/module_action_icon_size"
android:background="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintTop_toBottomOf="@id/description_text"
app:layout_constraintRight_toLeftOf="@id/button_action5"
android:importantForAccessibility="no"
android:layout_marginLeft="8dp"
tools:ignore="RtlHardcoded" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/fail_root_magisk"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,9 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/compat_menu_item"
android:enabled="false"
android:icon="@null"
android:title=""
app:showAsAction="always" />
</menu>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Thanks https://romannurik.github.io/AndroidAssetStudio/ for icons -->
<!--
https://romannurik.github.io/AndroidAssetStudio/icons-launcher.html#foreground.type=clipart&foreground.clipart=extension&foreground.space.trim=0&foreground.space.pad=0.25&foreColor=rgb(255%2C%20255%2C%20255)&backColor=rgb(255%2C%20152%2C%200)&crop=0&backgroundShape=circle&effects=elevate&name=ic_launcher
-->
<background android:drawable="@mipmap/ic_launcher_adaptive_back"/>
<foreground android:drawable="@mipmap/ic_launcher_adaptive_fore"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@ -0,0 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.MagiskModuleManager"
parent="Theme.MagiskModuleManager.Dark" />
</resources>

@ -0,0 +1,13 @@
<resources>
<string-array name="theme_values">
<item>system</item>
<item>dark</item>
<item>light</item>
</string-array>
<string-array name="theme_values_names">
<item>System</item>
<item>Dark</item>
<item>Light</item>
</string-array>
</resources>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="module_action_icon_size">32dp</dimen>
<dimen name="card_corner_radius">8dp</dimen>
<dimen name="markdown_side_clearance">0dp</dimen>
<dimen name="markdown_topdown_clearance">32dp</dimen>
<dimen name="markdown_border_content">0dp</dimen>
</resources>

@ -0,0 +1,40 @@
<resources>
<string name="app_name">Fox\'s Magisk Module Manager</string>
<string name="app_name_short">Fox\'s Mmm</string>
<string name="fail_root_magisk">Failed to get access to Root or Magisk</string>
<string name="loading">Loading…</string>
<string name="updatable">Updatable</string>
<string name="installed">Installed</string>
<string name="online_repo">Online Repo</string>
<string name="showcase_mode">The application is in showcase mode</string>
<string name="failed_download">Failed to download file.</string>
<string name="slow_modules">Modules took too long to boot, consider disabling some modules</string>
<string name="fail_internet">Fail to connect to the internet</string>
<string name="title_activity_settings">SettingsActivity</string>
<!-- Preference Titles -->
<string name="showcase_mode_pref">Showcase mode</string>
<string name="showcase_mode_desc">Showcase mode prevent manager to do action on modules</string>
<string name="pref_category_settings">Settings</string>
<string name="pref_category_info">Info</string>
<string name="show_licenses">Show licenses</string>
<string name="licenses">Licences</string>
<string name="show_incompatible_pref">Show incompatible modules</string>
<string name="show_incompatible_desc">Show modules that are incompatible with your device based on their metadata</string>
<string name="magisk_outdated">Magisk is outdated!</string>
<string name="pref_category_repos">Repos</string>
<string name="repo_main_desc">The repository hosting Magisk Modules</string>
<string name="repo_main_alt">An alternative to Magisk-Modules-Repo with fewer restrictions.</string>
<string name="master_delete">Delete the module files?</string>
<string name="master_delete_no">Keep files</string>
<string name="master_delete_yes">Delete files</string>
<string name="master_delete_fail">Failed to delete the module files</string>
<string name="theme_pref">Theme</string>
<string name="module_id_prefix">Module id: </string>
<string name="install_from_storage">Install module from storage</string>
<string name="invalid_format">The selected module is in an invalid format</string>
<string name="local_install_title">Local install</string>
<string name="source_code">Source code</string>
<string name="last_updated">Last update:</string>
<string name="magisk_builtin_module">Magisk builtin module</string>
</resources>

@ -0,0 +1,78 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.MagiskModuleManager.Light" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="android:windowActivityTransitions">true</item>
<!-- <item name="android:activityOpenEnterAnimation">@*android:anim/slide_in_right</item>
<item name="android:activityOpenExitAnimation">@*android:anim/slide_out_left</item>
<item name="android:activityCloseEnterAnimation">@*android:anim/slide_in_left</item>
<item name="android:activityCloseExitAnimation">@*android:anim/slide_out_right</item> -->
<item name="android:windowEnterAnimation">@android:anim/fade_in</item>
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style>
<!-- Base application theme. -->
<style name="Theme.MagiskModuleManager.Dark" parent="Theme.MaterialComponents">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="android:windowActivityTransitions">true</item>
<!-- <item name="android:activityOpenEnterAnimation">@*android:anim/slide_in_right</item>
<item name="android:activityOpenExitAnimation">@*android:anim/slide_out_left</item>
<item name="android:activityCloseEnterAnimation">@*android:anim/slide_in_left</item>
<item name="android:activityCloseExitAnimation">@*android:anim/slide_out_right</item> -->
<item name="android:windowEnterAnimation">@android:anim/fade_in</item>
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style>
<!-- Base application theme. -->
<style name="Theme.MagiskModuleManager.Transparent.Dark" parent="Theme.MagiskModuleManager.Dark">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.MagiskModuleManager.Transparent.Light" parent="Theme.MagiskModuleManager.Light">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.MagiskModuleManager"
parent="Theme.MagiskModuleManager.Light" />
<style name="Theme.MagiskModuleManager.Transparent"
parent="Theme.MagiskModuleManager.Transparent.Light" />
</resources>

@ -0,0 +1,52 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:title="@string/pref_category_settings">
<ListPreference
app:key="pref_theme"
app:icon="@drawable/ic_baseline_palette_24"
app:title="@string/theme_pref"
app:defaultValue="system"
app:entries="@array/theme_values_names"
app:entryValues="@array/theme_values" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:key="pref_showcase_mode"
app:icon="@drawable/ic_baseline_monitor_24"
app:title="@string/showcase_mode_pref"
app:summary="@string/showcase_mode_desc"/>
<SwitchPreferenceCompat
app:defaultValue="false"
app:key="pref_show_incompatible"
app:icon="@drawable/ic_baseline_hide_source_24"
app:title="@string/show_incompatible_pref"
app:summary="@string/show_incompatible_desc"/>
</PreferenceCategory>
<PreferenceCategory
app:title="@string/pref_category_repos">
<Preference
app:key="pref_repo_main"
app:icon="@drawable/ic_baseline_extension_24"
app:summary="@string/repo_main_desc"
app:title="@string/loading" />
<Preference
app:key="pref_repo_alt"
app:icon="@drawable/ic_baseline_extension_24"
app:summary="@string/repo_main_alt"
app:title="@string/loading" />
</PreferenceCategory>
<PreferenceCategory
app:title="@string/pref_category_info">
<Preference
app:key="pref_source_code"
app:icon="@drawable/ic_github"
app:title="@string/source_code" />
<Preference
app:key="pref_show_licenses"
app:icon="@drawable/ic_baseline_info_24"
app:title="@string/show_licenses" />
</PreferenceCategory>
</PreferenceScreen>

@ -0,0 +1,17 @@
package com.fox2code.mmm;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

@ -0,0 +1,20 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
project.ext.latestAboutLibsRelease = "8.9.1"
dependencies {
classpath "com.android.tools.build:gradle:7.0.2"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${latestAboutLibsRelease}"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

Binary file not shown.

@ -0,0 +1,28 @@
#!/bin/sh
if [ -n "$MMM_EXT_SUPPORT" ]; then
ui_print "#!useExt"
mmm_exec() {
ui_print "$(echo "#!$@")"
}
else
mmm_exec() { true; }
abort "! This module need to be executed in Fox's Magisk Module Manager"
exit 1
fi
ui_print "- Doing stuff"
ui_print "- Current state: LOADING"
mmm_exec showLoading
sleep 4
mmm_exec setLastLine "- Current state: LOADING AGAIN"
sleep 4
mmm_exec setLastLine "- Current state: LOADED"
mmm_exec hideLoading
ui_print "- Doing more stuff"
sleep 4
# You can even set youtube links as support links
# Note: Button only appear once install ended
mmm_exec setSupportLink "https://youtu.be/dQw4w9WgXcQ"
ui_print "- Modules installer can also set custom shortcut"
abort "! Check top right button to see where it goes"

@ -0,0 +1,11 @@
id=fox_mmm_example
name=Fox's Mmm Example Module
version=v1.0
versionCode=1
author=Fox2Code
description=Fox's Magisk Module Manager example module
minApi=21
minMagisk=19000
support=https://github.com/Fox2Code/FoxMagiskModuleManager
donate=https://paypal.me/fox2code
config=com.fox2code.mmm

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

Loading…
Cancel
Save