@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
@ -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)
|
@ -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'
|
||||||
|
}
|
@ -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>
|
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 6.5 KiB |
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
|
||||||
|
}
|
@ -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
|