Merge pull request #230 from Androidacy/master

Huge changes/update
pull/27/head
Androidacy Service Account 4 years ago committed by GitHub
commit 92952e112e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

1
.gitignore vendored

@ -9,3 +9,4 @@
.cxx
local.properties
sentry.properties
androidacy.properties

@ -1,6 +1,9 @@
# Fox's Magisk Module Manager
## Important
<details>
<summary>
Important news
</summary>
I have health problems that made me work slow on everything.
I don't like sharing my health problmes but it has been to much recently for me to keep it for myself.
@ -50,6 +53,8 @@ at least you won't be hurting peoples with mental/health issues by faking having
I'll probably delete this section once my health would be gotten better, or at
least good enough for me to not be stuck on my bed at least once a day because of pain.
</details>
## Community
[![Telegram Group](https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2FFox2Code_Chat)](https://telegram.dog/Fox2Code_Chat)
@ -94,9 +99,9 @@ and download and install the latest `.apk` on your device.
## Repositories Available
The app currently use these three repo as it's module sources, with it's benefits and drawback:
The app currently use these two repos as module sources, each with their own benefits and drawback:
(Note: Each module repo can be disabled in the settings of the app)
(Note²: I do not own or monitor any of the repo, **download at your own risk**)
(Note²: I do not own or actively monitor any of the repos or modules, **download at your own risk**)
#### [https://github.com/Magisk-Modules-Alt-Repo](https://github.com/Magisk-Modules-Alt-Repo)
- Accepting new modules [here](https://github.com/Magisk-Modules-Alt-Repo/submission)
@ -112,6 +117,7 @@ Support:
- Modules downloadable easily outside the app
- Officially supported by Fox's mmm
- Contains ads to help cover server costs
- Added features like module reviews, automatic VirusTotal scans, and more
Support:
@ -152,6 +158,13 @@ If your language is right to left don't forget to set `lang_support_rtl` to `tru
Translators are not expected to have any previous coding experience.
## License
See [LICENSE](LICENCE). Library licenses can be found in the licenses section of the app.
Cronet is licensed under the Apache License, Version 2.0. Static libraries are licensed under
the BSD license. See [LICENSE](https://chromium.googlesource.com/chromium/src/+/master/LICENSE)
for more information. Libraries were built using the microg build script which can be found [here](https://github.com/microg/cronet-build).
## I want to add my own repo
To add you own repo to Fox's mmm it need to follow theses conditions:

@ -1,6 +1,6 @@
plugins {
// Gradle doesn't allow conditionally enabling/disabling plugins
id "io.sentry.android.gradle" version "3.1.5"
id "io.sentry.android.gradle" version "3.3.0"
id 'com.android.application'
id 'com.mikepenz.aboutlibraries.plugin'
}
@ -8,29 +8,46 @@ plugins {
android {
namespace "com.fox2code.mmm"
compileSdk 33
buildToolsVersion '30.0.3'
signingConfigs {
release {
// Everything comes from local.properties
Properties properties = new Properties()
if (project.rootProject.file('local.properties').exists()) {
properties.load(project.rootProject.file('local.properties').newDataInputStream())
storeFile file(properties.getProperty('keystore.file'))
storePassword properties.getProperty('keystore.password')
keyAlias 'key0'
keyPassword properties.getProperty('keystore.password')
}
}
}
defaultConfig {
applicationId "com.fox2code.mmm"
minSdk 21
targetSdk 33
versionCode 59
versionName "0.6.7"
versionCode 60
versionName "0.6.8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
signingConfig signingConfigs.release
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
debug {
applicationIdSuffix '.debug'
debuggable true
// ONLY FOR TESTING SENTRY
// minifyEnabled true
// shrinkResources true
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),'proguard-rules.pro'
}
}
@ -40,9 +57,26 @@ android {
dimension "type"
buildConfigField "boolean", "ENABLE_AUTO_UPDATER", "true"
buildConfigField "boolean", "DEFAULT_ENABLE_CRASH_REPORTING", "true"
buildConfigField("java.util.List<String>",
"ENABLED_REPOS",
"java.util.Arrays.asList(\"magisk_alt_repo\", \"androidacy_repo\")",)
// Get the androidacy client ID from the androidacy.properties
Properties properties = new Properties()
// If androidacy.properties doesn't exist, use the default client ID which is heavily
// rate limited to 30 requests per minute
if (project.rootProject.file('androidacy.properties').exists()) {
properties.load(project.rootProject.file('androidacy.properties').newDataInputStream())
} else {
properties.setProperty('client_id', '"5KYccdYxWB2RxMq5FTbkWisXi2dS6yFN9R7RVlFCG98FRdz6Mf5ojY2fyJCUlXJZ"')
}
buildConfigField("String", "ANDROIDACY_CLIENT_ID", properties.getProperty('client_id'))
// If client ID is empty, disable androidacy
if (properties.getProperty('client_id').isEmpty()) {
buildConfigField("java.util.List<String>",
"ENABLED_REPOS", "java.util.Arrays.asList(\"magisk_alt_repo\")")
} else {
buildConfigField("java.util.List<String>",
"ENABLED_REPOS",
"java.util.Arrays.asList(\"magisk_alt_repo\", \"androidacy_repo\")",)
}
}
fdroid {
@ -62,6 +96,17 @@ android {
buildConfigField("java.util.List<String>",
"ENABLED_REPOS",
"java.util.Arrays.asList(\"magisk_alt_repo\")",)
// Get the androidacy client ID from the androidacy.properties
Properties properties = new Properties()
// If androidacy.properties doesn't exist, use the default client ID which is limited
// to 50 requests per minute
if (project.rootProject.file('androidacy.properties').exists()) {
properties.load(project.rootProject.file('androidacy.properties').newDataInputStream())
} else {
properties.setProperty('client_id', '"dQ1p7X8bF14PVJ7wAU6ORVjPB2IeTinsuAZ8Uos6tQiyUdUyIjSyZSmN54QBbaTy"')
}
buildConfigField("String", "ANDROIDACY_CLIENT_ID", properties.getProperty('client_id'))
}
}
@ -137,7 +182,7 @@ sentry {
// as Gradle will resolve it to the latest version.
//
// Defaults to the latest published sentry version.
sentryVersion = '6.5.0'
sentryVersion = '6.9.2'
}
}
@ -175,17 +220,20 @@ dependencies {
implementation 'androidx.work:work-runtime:2.7.1'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:5.0.0-alpha.10'
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.10'
// Chromium cronet from microG
implementation fileTree(dir: 'libs', include: '*.jar')
// Force prefer our own version of Cronet
implementation 'com.github.topjohnwu.libsu:io:5.0.1'
implementation 'com.github.Fox2Code:RosettaX:1.0.9'
implementation 'com.github.Fox2Code:AndroidANSI:1.0.1'
if (hasSentryConfig) {
// Error reporting
defaultImplementation 'io.sentry:sentry-android:6.5.0'
defaultImplementation 'io.sentry:sentry-android-fragment:6.5.0'
defaultImplementation 'io.sentry:sentry-android-okhttp:6.5.0'
defaultImplementation 'io.sentry:sentry-android-core:6.5.0'
defaultImplementation 'io.sentry:sentry-android-ndk:6.5.0'
defaultImplementation 'io.sentry:sentry-android:6.9.2'
defaultImplementation 'io.sentry:sentry-android-fragment:6.9.2'
defaultImplementation 'io.sentry:sentry-android-okhttp:6.9.2'
defaultImplementation 'io.sentry:sentry-android-core:6.9.2'
defaultImplementation 'io.sentry:sentry-android-ndk:6.9.2'
}
// Markdown
@ -193,13 +241,14 @@ dependencies {
implementation "io.noties.markwon:html:4.6.2"
implementation "io.noties.markwon:image:4.6.2"
implementation "io.noties.markwon:syntax-highlight:4.6.2"
implementation 'com.google.net.cronet:cronet-okhttp:0.1.0'
// Ignore all org.chromium.net dependencies
annotationProcessor "io.noties:prism4j-bundler:2.0.0"
implementation "com.caverock:androidsvg:1.4"
// Test
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
}
if (hasSentryConfig) {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -50,7 +50,7 @@
static void enableDebugLogging(boolean);
}
-assumevalues class androidx.loader.app.LoaderManagerImpl {
static boolean DEBUG return false;
static boolean DEBUG;
}
# This is just some proguard rules testes, might do a separate lib after
@ -186,3 +186,37 @@
int getSafeInsetTop();
android.graphics.Insets getWaterfallInsets();
}
# Keep all of Cronet API and google's internal classes
-keep class org.chromium.net.** { *; }
-keep class org.chromium.** { *; }
-keep class com.google.** { *; }
# Silence some warnings
-dontwarn android.os.SystemProperties
-dontwarn android.view.ThreadedRenderer
-dontwarn cyanogenmod.providers.CMSettings$Secure
-dontwarn lineageos.providers.LineageSettings$System
-dontwarn lineageos.style.StyleInterface
-dontwarn me.weishu.reflection.Reflection
-dontwarn org.lsposed.hiddenapibypass.HiddenApiBypass
-dontwarn rikka.core.res.ResourcesCompatLayoutInflaterListener
-dontwarn rikka.core.util.ResourceUtils
-dontwarn com.afollestad.materialdialogs.MaterialDialog
-dontwarn com.afollestad.materialdialogs.WhichButton
-dontwarn com.afollestad.materialdialogs.actions.DialogActionExtKt
-dontwarn com.afollestad.materialdialogs.callbacks.DialogCallbackExtKt
-dontwarn com.afollestad.materialdialogs.internal.button.DialogActionButton
-dontwarn com.afollestad.materialdialogs.internal.button.DialogActionButtonLayout
-dontwarn com.afollestad.materialdialogs.internal.main.DialogLayout
-dontwarn com.afollestad.materialdialogs.internal.main.DialogTitleLayout
-dontwarn com.afollestad.materialdialogs.internal.message.DialogContentLayout
-dontwarn com.oracle.svm.core.annotate.AutomaticFeature
-dontwarn com.oracle.svm.core.annotate.Delete
-dontwarn com.oracle.svm.core.annotate.Substitute
-dontwarn com.oracle.svm.core.annotate.TargetClass
-dontwarn com.oracle.svm.core.configure.ResourcesRegistry
-dontwarn javax.lang.model.element.Modifier
-dontwarn org.graalvm.nativeimage.ImageSingletons
-dontwarn org.graalvm.nativeimage.hosted.Feature$BeforeAnalysisAccess
-dontwarn org.graalvm.nativeimage.hosted.Feature

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

@ -1,25 +1,33 @@
package com.fox2code.mmm;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.cardview.widget.CardView;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
@ -39,16 +47,12 @@ import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.NoodleDebug;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.topjohnwu.superuser.Shell;
import eightbitlab.com.blurview.BlurView;
import eightbitlab.com.blurview.RenderEffectBlur;
import eightbitlab.com.blurview.RenderScriptBlur;
public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRefreshListener,
SearchView.OnQueryTextListener, SearchView.OnCloseListener,
OverScrollManager.OverScrollHelper {
public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRefreshListener, SearchView.OnQueryTextListener, SearchView.OnCloseListener, OverScrollManager.OverScrollHelper {
private static final String TAG = "MainActivity";
private static final int PRECISION = 10000;
public static boolean noodleDebugState = BuildConfig.DEBUG;
@ -87,15 +91,13 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
noodleDebugState = MainApplication.isDeveloper();
BackgroundUpdateChecker.onMainActivityCreate(this);
super.onCreate(savedInstanceState);
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_settings_24, v -> {
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_settings_24, v -> {
IntentHelper.startActivity(this, SettingsActivity.class);
return true;
}, R.string.pref_category_settings);
return true;
}, R.string.pref_category_settings);
setContentView(R.layout.activity_main);
this.setTitle(R.string.app_name);
this.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
setActionBarBackground(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams layoutParams = this.getWindow().getAttributes();
@ -108,10 +110,8 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
this.actionBarBackground = new ColorDrawable(Color.TRANSPARENT);
this.progressIndicator = findViewById(R.id.progress_bar);
this.swipeRefreshLayout = findViewById(R.id.swipe_refresh);
this.swipeRefreshLayoutOrigStartOffset =
this.swipeRefreshLayout.getProgressViewStartOffset();
this.swipeRefreshLayoutOrigEndOffset =
this.swipeRefreshLayout.getProgressViewEndOffset();
this.swipeRefreshLayoutOrigStartOffset = this.swipeRefreshLayout.getProgressViewStartOffset();
this.swipeRefreshLayoutOrigEndOffset = this.swipeRefreshLayout.getProgressViewEndOffset();
this.swipeRefreshBlocker = Long.MAX_VALUE;
this.moduleList = findViewById(R.id.module_list);
this.searchCard = findViewById(R.id.search_card);
@ -134,8 +134,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
});
this.searchCard.setRadius(this.searchCard.getHeight() / 2F);
this.searchView.setMinimumHeight(FoxDisplay.dpToPixel(16));
this.searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH |
EditorInfo.IME_FLAG_NO_FULLSCREEN);
this.searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH | EditorInfo.IME_FLAG_NO_FULLSCREEN);
this.searchView.setOnQueryTextListener(this);
this.searchView.setOnCloseListener(this);
this.searchView.setOnQueryTextFocusChangeListener((v, h) -> {
@ -155,8 +154,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
@Override
public void onPathReceived(String path) {
Log.i(TAG, "Got magisk path: " + path);
if (InstallerInitializer.peekMagiskVersion() <
Constants.MAGISK_VER_CODE_INSTALL_COMMAND)
if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND)
moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED);
if (!MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
@ -166,8 +164,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
ensurePermissions();
noodleDebug.pop();
ModuleManager.getINSTANCE().scan();
ModuleManager.getINSTANCE().runAfterScan(
moduleViewListBuilder::appendInstalledModules);
ModuleManager.getINSTANCE().runAfterScan(moduleViewListBuilder::appendInstalledModules);
this.commonNext();
}
@ -176,8 +173,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
Log.i(TAG, "Failed to get magisk path!");
noodleDebug.setEnabled(noodleDebugState);
noodleDebug.bind();
moduleViewListBuilder.addNotification(
InstallerInitializer.getErrorNotification());
moduleViewListBuilder.addNotification(InstallerInitializer.getErrorNotification());
this.commonNext();
}
@ -206,14 +202,12 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
noodleDebug.replace("Check Update Compat");
AppUpdateManager.getAppUpdateManager().checkUpdateCompat();
noodleDebug.replace("Check Update");
RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () ->
progressIndicator.setProgressCompat(
(int) (value * PRECISION), true) :() ->
progressIndicator.setProgressCompat(
(int) (value * PRECISION * 0.75F), true)));
RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat((int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat((int) (value * PRECISION * 0.75F), true)));
NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder);
if (!NotificationType.NO_INTERNET.shouldRemove()) {
moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
} else if (!NotificationType.REPO_UPDATE_FAILED.shouldRemove()) {
moduleViewListBuilder.addNotification(NotificationType.REPO_UPDATE_FAILED);
} else {
// Compatibility data still needs to be updated
AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager();
@ -224,21 +218,17 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
if (max != 0) {
int current = 0;
noodleDebug.push("");
for (LocalModuleInfo localModuleInfo :
ModuleManager.getINSTANCE().getModules().values()) {
for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) {
if (localModuleInfo.updateJson != null) {
noodleDebug.replace(localModuleInfo.id);
try {
localModuleInfo.checkModuleUpdate();
} catch (Exception e) {
Log.e("MainActivity", "Failed to fetch update of: "
+ localModuleInfo.id, e);
Log.e("MainActivity", "Failed to fetch update of: " + localModuleInfo.id, e);
}
current++;
final int currentTmp = current;
runOnUiThread(() -> progressIndicator.setProgressCompat(
(int) ((1F * currentTmp / max) * PRECISION * 0.25F
+ (PRECISION * 0.75F)), true));
runOnUiThread(() -> progressIndicator.setProgressCompat((int) ((1F * currentTmp / max) * PRECISION * 0.25F + (PRECISION * 0.75F)), true));
}
}
noodleDebug.pop();
@ -252,8 +242,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
updateScreenInsets(getResources().getConfiguration());
});
noodleDebug.replace("Apply");
RepoManager.getINSTANCE().runAfterUpdate(
moduleViewListBuilder::appendRemoteModules);
RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilder::appendRemoteModules);
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
noodleDebug.pop();
Log.i(TAG, "Finished app opening state!");
@ -266,8 +255,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
private void cardIconifyUpdate() {
boolean iconified = this.searchView.isIconified();
int backgroundAttr = iconified ? MainApplication.isMonetEnabled() ?
R.attr.colorSecondaryContainer : // Monet is special...
int backgroundAttr = iconified ? MainApplication.isMonetEnabled() ? R.attr.colorSecondaryContainer : // Monet is special...
R.attr.colorSecondary : R.attr.colorPrimarySurface;
Resources.Theme theme = this.searchCard.getContext().getTheme();
TypedValue value = new TypedValue();
@ -277,48 +265,38 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
}
private void updateScreenInsets() {
this.runOnUiThread(() -> this.updateScreenInsets(
this.getResources().getConfiguration()));
this.runOnUiThread(() -> this.updateScreenInsets(this.getResources().getConfiguration()));
}
private void updateScreenInsets(Configuration configuration) {
boolean landscape = configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE;
boolean landscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE;
int bottomInset = (landscape ? 0 : this.getNavigationBarHeight());
int statusBarHeight = getStatusBarHeight();
int actionBarHeight = getActionBarHeight();
int combinedBarsHeight = statusBarHeight + actionBarHeight;
this.actionBarPadding.setMinHeight(combinedBarsHeight);
this.swipeRefreshLayout.setProgressViewOffset(false,
swipeRefreshLayoutOrigStartOffset + combinedBarsHeight,
swipeRefreshLayoutOrigEndOffset + combinedBarsHeight);
this.moduleViewListBuilder.setHeaderPx(Math.max(statusBarHeight,
combinedBarsHeight - FoxDisplay.dpToPixel(4)));
this.moduleViewListBuilder.setFooterPx(FoxDisplay.dpToPixel(4) +
bottomInset + this.searchCard.getHeight());
this.swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayoutOrigStartOffset + combinedBarsHeight, swipeRefreshLayoutOrigEndOffset + combinedBarsHeight);
this.moduleViewListBuilder.setHeaderPx(Math.max(statusBarHeight, combinedBarsHeight - FoxDisplay.dpToPixel(4)));
this.moduleViewListBuilder.setFooterPx(FoxDisplay.dpToPixel(4) + bottomInset + this.searchCard.getHeight());
this.searchCard.setRadius(this.searchCard.getHeight() / 2F);
this.moduleViewListBuilder.updateInsets();
//this.actionBarBlur.invalidate();
this.overScrollInsetTop = combinedBarsHeight;
this.overScrollInsetBottom = bottomInset;
Log.d(TAG, "( " + bottomInset + ", " +
this.searchCard.getHeight() + ")");
Log.d(TAG, "( " + bottomInset + ", " + this.searchCard.getHeight() + ")");
}
private void updateBlurState() {
boolean isLightMode = this.isLightTheme();
int colorBackground;
try {
colorBackground = this.getColorCompat(
android.R.attr.windowBackground);
colorBackground = this.getColorCompat(android.R.attr.windowBackground);
} catch (Resources.NotFoundException e) {
colorBackground = this.getColorCompat(isLightMode ?
R.color.white : R.color.black);
colorBackground = this.getColorCompat(isLightMode ? R.color.white : R.color.black);
}
if (MainApplication.isBlurEnabled()) {
this.actionBarBlur.setBlurEnabled(true);
this.actionBarBackground.setColor(ColorUtils
.setAlphaComponent(colorBackground, 0x02));
this.actionBarBackground.setColor(ColorUtils.setAlphaComponent(colorBackground, 0x02));
this.actionBarBackground.setColor(Color.TRANSPARENT);
} else {
this.actionBarBlur.setBlurEnabled(false);
@ -346,23 +324,20 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
@Override
public void onPathReceived(String path) {
if (InstallerInitializer.peekMagiskVersion() <
Constants.MAGISK_VER_CODE_INSTALL_COMMAND)
if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND)
moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED);
if (!MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
noodleDebug.setEnabled(noodleDebugState);
noodleDebug.bind();
ModuleManager.getINSTANCE().scan();
ModuleManager.getINSTANCE().runAfterScan(
moduleViewListBuilder::appendInstalledModules);
ModuleManager.getINSTANCE().runAfterScan(moduleViewListBuilder::appendInstalledModules);
this.commonNext();
}
@Override
public void onFailure(int error) {
moduleViewListBuilder.addNotification(
InstallerInitializer.getErrorNotification());
moduleViewListBuilder.addNotification(InstallerInitializer.getErrorNotification());
noodleDebug.setEnabled(noodleDebugState);
noodleDebug.bind();
this.commonNext();
@ -386,17 +361,14 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
progressIndicator.setMax(PRECISION);
});
noodleDebug.replace("Check Update");
RepoManager.getINSTANCE().update(value -> runOnUiThread(() ->
progressIndicator.setProgressCompat(
(int) (value * PRECISION), true)));
RepoManager.getINSTANCE().update(value -> runOnUiThread(() -> progressIndicator.setProgressCompat((int) (value * PRECISION), true)));
runOnUiThread(() -> {
progressIndicator.setProgressCompat(PRECISION, true);
progressIndicator.setVisibility(View.GONE);
});
}
noodleDebug.replace("Apply");
RepoManager.getINSTANCE().runAfterUpdate(
moduleViewListBuilder::appendRemoteModules);
RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilder::appendRemoteModules);
Log.i(TAG, "Common Before applyTo");
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
noodleDebug.pop();
@ -414,9 +386,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
@Override
public void onRefresh() {
if (this.swipeRefreshBlocker > System.currentTimeMillis() ||
this.initMode || this.progressIndicator == null ||
this.progressIndicator.getVisibility() == View.VISIBLE) {
if (this.swipeRefreshBlocker > System.currentTimeMillis() || this.initMode || this.progressIndicator == null || this.progressIndicator.getVisibility() == View.VISIBLE) {
this.swipeRefreshLayout.setRefreshing(false);
return; // Do not double scan
}
@ -430,45 +400,37 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
Http.cleanDnsCache(); // Allow DNS reload from network
noodleDebug.push("Check Update");
final int max = ModuleManager.getINSTANCE().getUpdatableModuleCount();
RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () ->
progressIndicator.setProgressCompat(
(int) (value * PRECISION), true) :() ->
progressIndicator.setProgressCompat(
(int) (value * PRECISION * 0.75F), true)));
RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat((int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat((int) (value * PRECISION * 0.75F), true)));
NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder);
if (!NotificationType.NO_INTERNET.shouldRemove()) {
moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
} else {
// Compatibility data still needs to be updated
AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager();
noodleDebug.replace("Check App Update");
// noodleDebug.replace("Check App Update");
if (BuildConfig.ENABLE_AUTO_UPDATER && appUpdateManager.checkUpdate(true))
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
noodleDebug.replace("Check Json Update");
// noodleDebug.replace("Check Json Update");
if (max != 0) {
int current = 0;
noodleDebug.push("");
for (LocalModuleInfo localModuleInfo :
ModuleManager.getINSTANCE().getModules().values()) {
for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) {
if (localModuleInfo.updateJson != null) {
noodleDebug.replace(localModuleInfo.id);
try {
localModuleInfo.checkModuleUpdate();
} catch (Exception e) {
Log.e("MainActivity", "Failed to fetch update of: "
+ localModuleInfo.id, e);
Log.e("MainActivity", "Failed to fetch update of: " + localModuleInfo.id, e);
}
current++;
final int currentTmp = current;
runOnUiThread(() -> progressIndicator.setProgressCompat(
(int) ((1F * currentTmp / max) * PRECISION * 0.25F
+ (PRECISION * 0.75F)), true));
runOnUiThread(() -> progressIndicator.setProgressCompat((int) ((1F * currentTmp / max) * PRECISION * 0.25F + (PRECISION * 0.75F)), true));
}
}
noodleDebug.pop();
}
}
noodleDebug.replace("Apply");
// noodleDebug.replace("Apply");
runOnUiThread(() -> {
this.progressIndicator.setVisibility(View.GONE);
this.swipeRefreshLayout.setRefreshing(false);
@ -478,12 +440,11 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
this.moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
}
RepoManager.getINSTANCE().updateEnabledStates();
RepoManager.getINSTANCE().runAfterUpdate(
moduleViewListBuilder::appendRemoteModules);
RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilder::appendRemoteModules);
this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
noodleDebug.pop();
noodleDebug.unbind();
},"Repo update thread").start();
// noodleDebug.pop();
// noodleDebug.unbind();
}, "Repo update thread").start();
}
@Override
@ -491,8 +452,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
this.searchView.clearFocus();
if (this.initMode) return false;
if (this.moduleViewListBuilder.setQueryChange(query)) {
new Thread(() -> this.moduleViewListBuilder.applyTo(
moduleList, moduleViewAdapter), "Query update thread").start();
new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();
}
return true;
}
@ -501,8 +461,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
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();
new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();
}
return false;
}
@ -511,8 +470,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
public boolean onClose() {
if (this.initMode) return false;
if (this.moduleViewListBuilder.setQueryChange(null)) {
new Thread(() -> this.moduleViewListBuilder.applyTo(
moduleList, moduleViewAdapter), "Query update thread").start();
new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start();
}
return false;
}
@ -527,14 +485,64 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
return this.overScrollInsetBottom;
}
@SuppressLint("RestrictedApi")
private void ensurePermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this,
Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED) {
// TODO Use standard Android API to ask for permissions
Shell.cmd("pm grant " + this.getPackageName() + " " +
Manifest.permission.POST_NOTIFICATIONS);
// First, check if user has said don't ask again by checking if pref_dont_ask_again_notification_permission is true
if (!PreferenceManager.getDefaultSharedPreferences(this).getBoolean("pref_dont_ask_again_notification_permission", false)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
// Show a dialog explaining why we need this permission, which is to show
// notifications for updates
runOnUiThread(() -> {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.permission_notification_title);
builder.setMessage(R.string.permission_notification_message);
// Don't ask again checkbox
View view = getLayoutInflater().inflate(R.layout.dialog_checkbox, null);
CheckBox checkBox = view.findViewById(R.id.checkbox);
checkBox.setText(R.string.dont_ask_again);
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("pref_dont_ask_again_notification_permission", isChecked).apply());
builder.setView(view);
builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> {
// Request the permission
this.requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 0);
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
// Set pref_background_update_check to false and dismiss dialog
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
prefs.edit().putBoolean("pref_background_update_check", false).apply();
dialog.dismiss();
});
builder.show();
});
// Next branch is for < android 13 and user has blocked notifications
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && !NotificationManagerCompat.from(this).areNotificationsEnabled()) {
runOnUiThread(() -> {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.permission_notification_title);
builder.setMessage(R.string.permission_notification_message);
// Don't ask again checkbox
View view = getLayoutInflater().inflate(R.layout.dialog_checkbox, null);
CheckBox checkBox = view.findViewById(R.id.checkbox);
checkBox.setText(R.string.dont_ask_again);
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("pref_dont_ask_again_notification_permission", isChecked).apply());
builder.setView(view);
builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> {
// Open notification settings
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
// Set pref_background_update_check to false and dismiss dialog
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
prefs.edit().putBoolean("pref_background_update_check", false).apply();
dialog.dismiss();
});
builder.show();
});
}
}
}
}

@ -2,6 +2,7 @@ package com.fox2code.mmm;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
@ -14,6 +15,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.core.app.NotificationManagerCompat;
import androidx.emoji2.text.DefaultEmojiCompatConfig;
import androidx.emoji2.text.EmojiCompat;
import androidx.emoji2.text.FontRequestEmojiCompatConfig;
@ -32,6 +34,7 @@ import com.topjohnwu.superuser.Shell;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import io.noties.markwon.Markwon;
@ -45,35 +48,33 @@ import io.noties.markwon.syntax.SyntaxHighlightPlugin;
import io.noties.prism4j.Prism4j;
import io.noties.prism4j.annotations.PrismBundle;
@PrismBundle(
includeAll = true,
grammarLocatorClassName = ".Prism4jGrammarLocator"
)
public class MainApplication extends FoxApplication
implements androidx.work.Configuration.Provider {
@PrismBundle(includeAll = true, grammarLocatorClassName = ".Prism4jGrammarLocator")
public class MainApplication extends FoxApplication implements androidx.work.Configuration.Provider {
private static final String TAG = "MainApplication";
private static final String timeFormatString = "dd MMM yyyy"; // Example: 13 july 2001
private static Locale timeFormatLocale =
Resources.getSystem().getConfiguration().locale;
private static SimpleDateFormat timeFormat =
new SimpleDateFormat(timeFormatString, timeFormatLocale);
private static final Shell.Builder shellBuilder;
private static final long secret;
@SuppressLint("RestrictedApi") // Use FoxProcess wrapper helper.
private static final boolean wrapped = !FoxProcessExt.isRootLoader();
private static Locale timeFormatLocale = Resources.getSystem().getConfiguration().locale;
private static SimpleDateFormat timeFormat = new SimpleDateFormat(timeFormatString, timeFormatLocale);
private static SharedPreferences bootSharedPreferences;
private static String relPackageName = BuildConfig.APPLICATION_ID;
private static MainApplication INSTANCE;
private static boolean firstBoot;
static {
Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create()
.setFlags(Shell.FLAG_REDIRECT_STDERR)
.setTimeout(10).setInitializers(InstallerInitializer.class)
);
Shell.setDefaultBuilder(shellBuilder = Shell.Builder.create().setFlags(Shell.FLAG_REDIRECT_STDERR).setTimeout(10).setInitializers(InstallerInitializer.class));
secret = new Random().nextLong();
}
// Provides the Context for the base application
public Context FoxApplication = this;
@StyleRes
private int managerThemeResId = R.style.Theme_MagiskModuleManager;
private FoxThemeWrapper markwonThemeContext;
private Markwon markwon;
public MainApplication() {
if (INSTANCE != null && INSTANCE != this)
throw new IllegalStateException("Duplicate application instance!");
@ -86,10 +87,8 @@ public class MainApplication extends FoxApplication
public static void addSecret(Intent intent) {
ComponentName componentName = intent.getComponent();
String packageName = componentName != null ?
componentName.getPackageName() : intent.getPackage();
if (!BuildConfig.APPLICATION_ID.equalsIgnoreCase(packageName) &&
!relPackageName.equals(packageName)) {
String packageName = componentName != null ? componentName.getPackageName() : intent.getPackage();
if (!BuildConfig.APPLICATION_ID.equalsIgnoreCase(packageName) && !relPackageName.equals(packageName)) {
// Code safeguard, we should never reach here.
throw new IllegalArgumentException("Can't add secret to outbound Intent");
}
@ -135,29 +134,23 @@ public class MainApplication extends FoxApplication
}
public static boolean isMonetEnabled() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
getSharedPreferences().getBoolean("pref_enable_monet", true);
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && getSharedPreferences().getBoolean("pref_enable_monet", true);
}
public static boolean isBlurEnabled() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
getSharedPreferences().getBoolean("pref_enable_blur", false);
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && getSharedPreferences().getBoolean("pref_enable_blur", false);
}
public static boolean isDeveloper() {
return BuildConfig.DEBUG ||
getSharedPreferences().getBoolean("developer", false);
return BuildConfig.DEBUG || getSharedPreferences().getBoolean("developer", false);
}
public static boolean isDisableLowQualityModuleFilter() {
return getSharedPreferences().getBoolean("pref_disable_low_quality_module_filter",
false) && isDeveloper();
return getSharedPreferences().getBoolean("pref_disable_low_quality_module_filter", false) && isDeveloper();
}
public static boolean isUsingMagiskCommand() {
return InstallerInitializer.peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND
&& getSharedPreferences().getBoolean("pref_use_magisk_install_command", false)
&& isDeveloper();
return InstallerInitializer.peekMagiskVersion() >= Constants.MAGISK_VER_CODE_INSTALL_COMMAND && getSharedPreferences().getBoolean("pref_use_magisk_install_command", false) && isDeveloper();
}
public static boolean isBackgroundUpdateCheckEnabled() {
@ -165,8 +158,7 @@ public class MainApplication extends FoxApplication
}
public static boolean isAndroidacyTestMode() {
return isDeveloper() &&
getSharedPreferences().getBoolean("pref_androidacy_test_mode", false);
return isDeveloper() && getSharedPreferences().getBoolean("pref_androidacy_test_mode", false);
}
public static boolean isFirstBoot() {
@ -182,8 +174,7 @@ public class MainApplication extends FoxApplication
}
public static boolean isCrashReportingEnabled() {
return getSharedPreferences().getBoolean("pref_crash_reporting",
BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING && !BuildConfig.DEBUG);
return getSharedPreferences().getBoolean("pref_crash_reporting", BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING);
}
public static SharedPreferences getBootSharedPreferences() {
@ -199,24 +190,17 @@ public class MainApplication extends FoxApplication
return timeFormat.format(new Date(timeStamp));
}
@StyleRes
private int managerThemeResId = R.style.Theme_MagiskModuleManager;
private FoxThemeWrapper markwonThemeContext;
private Markwon markwon;
public static boolean isNotificationPermissionGranted() {
return NotificationManagerCompat.from(INSTANCE).areNotificationsEnabled();
}
public Markwon getMarkwon() {
if (this.markwon != null)
return this.markwon;
if (this.markwon != null) return this.markwon;
FoxThemeWrapper contextThemeWrapper = this.markwonThemeContext;
if (contextThemeWrapper == null) {
contextThemeWrapper = this.markwonThemeContext =
new FoxThemeWrapper(this, this.managerThemeResId);
contextThemeWrapper = this.markwonThemeContext = new FoxThemeWrapper(this, this.managerThemeResId);
}
Markwon markwon = Markwon.builder(contextThemeWrapper).usePlugin(HtmlPlugin.create())
.usePlugin(SyntaxHighlightPlugin.create(
new Prism4j(new Prism4jGrammarLocator()), new Prism4jSwitchTheme()))
.usePlugin(ImagesPlugin.create().addSchemeHandler(
OkHttpNetworkSchemeHandler.create(Http.getHttpClientWithCache()))).build();
Markwon markwon = Markwon.builder(contextThemeWrapper).usePlugin(HtmlPlugin.create()).usePlugin(SyntaxHighlightPlugin.create(new Prism4j(new Prism4jGrammarLocator()), new Prism4jSwitchTheme())).usePlugin(ImagesPlugin.create().addSchemeHandler(OkHttpNetworkSchemeHandler.create(Http.getHttpClientWithCache()))).build();
return this.markwon = markwon;
}
@ -230,40 +214,6 @@ public class MainApplication extends FoxApplication
return new androidx.work.Configuration.Builder().build();
}
private class Prism4jSwitchTheme implements Prism4jTheme {
private final Prism4jTheme light = new Prism4jThemeDefault(Color.TRANSPARENT);
private final Prism4jTheme dark = new Prism4jThemeDarkula(Color.TRANSPARENT);
private Prism4jTheme getTheme() {
return isLightTheme() ? this.light : this.dark;
}
@Override
public int background() {
return this.getTheme().background();
}
@Override
public int textColor() {
return this.getTheme().textColor();
}
@Override
public void apply(@NonNull String language, @NonNull Prism4j.Syntax syntax,
@NonNull SpannableStringBuilder builder, int start, int end) {
this.getTheme().apply(language, syntax, builder, start, end);
}
}
@SuppressLint("NonConstantResourceId")
public void setManagerThemeResId(@StyleRes int resId) {
this.managerThemeResId = resId;
if (this.markwonThemeContext != null) {
this.markwonThemeContext.setTheme(resId);
}
this.markwon = null;
}
public void updateTheme() {
@StyleRes int themeResId;
String theme;
@ -272,19 +222,16 @@ public class MainApplication extends FoxApplication
default:
Log.w("MainApplication", "Unknown theme id: " + theme);
case "system":
themeResId = monet ?
R.style.Theme_MagiskModuleManager_Monet :
R.style.Theme_MagiskModuleManager;
themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet : R.style.Theme_MagiskModuleManager;
break;
case "dark":
themeResId = monet ?
R.style.Theme_MagiskModuleManager_Monet_Dark :
R.style.Theme_MagiskModuleManager_Dark;
themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet_Dark : R.style.Theme_MagiskModuleManager_Dark;
break;
case "black":
themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet_Black : R.style.Theme_MagiskModuleManager_Black;
break;
case "light":
themeResId = monet ?
R.style.Theme_MagiskModuleManager_Monet_Light :
R.style.Theme_MagiskModuleManager_Light;
themeResId = monet ? R.style.Theme_MagiskModuleManager_Monet_Light : R.style.Theme_MagiskModuleManager_Light;
break;
}
this.setManagerThemeResId(themeResId);
@ -295,14 +242,21 @@ public class MainApplication extends FoxApplication
return managerThemeResId;
}
@SuppressLint("NonConstantResourceId")
public void setManagerThemeResId(@StyleRes int resId) {
this.managerThemeResId = resId;
if (this.markwonThemeContext != null) {
this.markwonThemeContext.setTheme(resId);
}
this.markwon = null;
}
@SuppressLint("NonConstantResourceId")
public boolean isLightTheme() {
switch (this.managerThemeResId) {
case R.style.Theme_MagiskModuleManager:
case R.style.Theme_MagiskModuleManager_Monet:
return (this.getResources().getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK)
!= Configuration.UI_MODE_NIGHT_YES;
return (this.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) != Configuration.UI_MODE_NIGHT_YES;
case R.style.Theme_MagiskModuleManager_Monet_Light:
case R.style.Theme_MagiskModuleManager_Light:
return true;
@ -314,6 +268,11 @@ public class MainApplication extends FoxApplication
}
}
@SuppressLint("NonConstantResourceId")
public boolean isDarkTheme() {
return !this.isLightTheme();
}
@Override
public void onCreate() {
if (INSTANCE == null) INSTANCE = this;
@ -326,14 +285,12 @@ public class MainApplication extends FoxApplication
}*/
SharedPreferences sharedPreferences = MainApplication.getSharedPreferences();
// We are only one process so it's ok to do this
SharedPreferences bootPrefs = MainApplication.bootSharedPreferences =
this.getSharedPreferences("mmm_boot", MODE_PRIVATE);
SharedPreferences bootPrefs = MainApplication.bootSharedPreferences = this.getSharedPreferences("mmm_boot", MODE_PRIVATE);
long lastBoot = System.currentTimeMillis() - SystemClock.elapsedRealtime();
long lastBootPrefs = bootPrefs.getLong("last_boot", 0);
if (lastBootPrefs == 0 || Math.abs(lastBoot - lastBootPrefs) > 100) {
boolean firstBoot = sharedPreferences.getBoolean("first_boot", true);
bootPrefs.edit().clear().putLong("last_boot", lastBoot)
.putBoolean("first_boot", firstBoot).apply();
bootPrefs.edit().clear().putLong("last_boot", lastBoot).putBoolean("first_boot", firstBoot).apply();
if (firstBoot) {
sharedPreferences.edit().putBoolean("first_boot", false).apply();
}
@ -347,12 +304,10 @@ public class MainApplication extends FoxApplication
// Update SSL Ciphers if update is possible
GMSProviderInstaller.installIfNeeded(this);
// Update emoji config
FontRequestEmojiCompatConfig fontRequestEmojiCompatConfig =
DefaultEmojiCompatConfig.create(this);
FontRequestEmojiCompatConfig fontRequestEmojiCompatConfig = DefaultEmojiCompatConfig.create(this);
if (fontRequestEmojiCompatConfig != null) {
fontRequestEmojiCompatConfig.setReplaceAll(true);
fontRequestEmojiCompatConfig
.setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
fontRequestEmojiCompatConfig.setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
EmojiCompat emojiCompat = EmojiCompat.init(fontRequestEmojiCompatConfig);
new Thread(() -> {
Log.d("MainApplication", "Loading emoji compat...");
@ -362,6 +317,13 @@ public class MainApplication extends FoxApplication
}
SentryMain.initialize(this);
if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) {
Log.w("MainApplication", "Androidacy client id is empty! Please set it in androidacy" +
".properties. Will not enable Androidacy.");
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean("pref_androidacy_repo_enabled", false);
editor.apply();
}
}
@Override
@ -381,9 +343,35 @@ public class MainApplication extends FoxApplication
Locale newTimeFormatLocale = newConfig.locale;
if (timeFormatLocale != newTimeFormatLocale) {
timeFormatLocale = newTimeFormatLocale;
timeFormat = new SimpleDateFormat(
timeFormatString, timeFormatLocale);
timeFormat = new SimpleDateFormat(timeFormatString, timeFormatLocale);
}
super.onConfigurationChanged(newConfig);
}
private class Prism4jSwitchTheme implements Prism4jTheme {
private final Prism4jTheme light = new Prism4jThemeDefault(Color.TRANSPARENT);
private final Prism4jTheme dark = new Prism4jThemeDarkula(Color.TRANSPARENT);
// Black theme
private final Prism4jTheme black = new Prism4jThemeDefault(Color.BLACK);
private Prism4jTheme getTheme() {
// isLightTheme() means light, isDarkTheme() means dark, and isBlackTheme() means black
return isLightTheme() ? light : isDarkTheme() ? dark : black;
}
@Override
public int background() {
return this.getTheme().background();
}
@Override
public int textColor() {
return this.getTheme().textColor();
}
@Override
public void apply(@NonNull String language, @NonNull Prism4j.Syntax syntax, @NonNull SpannableStringBuilder builder, int start, int end) {
this.getTheme().apply(language, syntax, builder, start, end);
}
}
}

@ -53,9 +53,7 @@ public enum NotificationType implements NotificationTypeCst {
return InstallerInitializer.getErrorNotification() != this;
}
},
MAGISK_OUTDATED(R.string.magisk_outdated, R.drawable.ic_baseline_update_24, v -> {
IntentHelper.openUrl(v.getContext(), "https://github.com/topjohnwu/Magisk/releases");
}) {
MAGISK_OUTDATED(R.string.magisk_outdated, R.drawable.ic_baseline_update_24, v -> IntentHelper.openUrl(v.getContext(), "https://github.com/topjohnwu/Magisk/releases")) {
@Override
public boolean shouldRemove() {
return InstallerInitializer.peekMagiskPath() == null ||
@ -70,6 +68,12 @@ public enum NotificationType implements NotificationTypeCst {
RepoManager.getINSTANCE().hasConnectivity();
}
},
REPO_UPDATE_FAILED(R.string.repo_update_failed, R.drawable.ic_baseline_cloud_off_24) {
@Override
public boolean shouldRemove() {
return RepoManager.getINSTANCE().isLastUpdateSuccess();
}
},
NEED_CAPTCHA_ANDROIDACY(R.string.androidacy_need_captcha, R.drawable.ic_baseline_refresh_24, v ->
IntentHelper.openUrlAndroidacy(v.getContext(),
"https://" + Http.needCaptchaAndroidacyHost() + "/", false)) {
@ -86,10 +90,8 @@ public enum NotificationType implements NotificationTypeCst {
}
},
UPDATE_AVAILABLE(R.string.app_update_available, R.drawable.ic_baseline_system_update_24,
R.attr.colorPrimary, R.attr.colorOnPrimary, v -> {
IntentHelper.openUrl(v.getContext(),
"https://github.com/Fox2Code/FoxMagiskModuleManager/releases");
}, false) {
R.attr.colorPrimary, R.attr.colorOnPrimary, v -> IntentHelper.openUrl(v.getContext(),
"https://github.com/Fox2Code/FoxMagiskModuleManager/releases"), false) {
@Override
public boolean shouldRemove() {
return !AppUpdateManager.getAppUpdateManager().peekShouldUpdate();

@ -37,13 +37,11 @@ import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
/**
@ -74,8 +72,7 @@ public final class AndroidacyActivity extends FoxActivity {
super.onCreate(savedInstanceState);
Intent intent = this.getIntent();
Uri uri;
if (!MainApplication.checkSecret(intent) ||
(uri = intent.getData()) == null) {
if (!MainApplication.checkSecret(intent) || (uri = intent.getData()) == null) {
Log.w(TAG, "Impersonation detected");
this.forceBackPressed();
return;
@ -99,8 +96,25 @@ public final class AndroidacyActivity extends FoxActivity {
url = url + '?' + AndroidacyUtil.REFERRER;
}
}
boolean allowInstall = intent.getBooleanExtra(
Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, false);
// Add token to url if not present
String token = uri.getQueryParameter("token");
if (token == null) {
// get from shared preferences
token = MainApplication.getSharedPreferences().getString("pref_androidacy_api_token", null);
url = url + "&token=" + token;
}
// Add device_id to url if not present
String device_id = uri.getQueryParameter("device_id");
if (device_id == null) {
// get from shared preferences
try {
device_id = AndroidacyRepoData.generateDeviceId();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
url = url + "&device_id=" + device_id;
}
boolean allowInstall = intent.getBooleanExtra(Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, false);
String title = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_TITLE);
String config = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG);
int compatLevel = intent.getIntExtra(Constants.EXTRA_ANDROIDACY_COMPAT_LEVEL, 0);
@ -119,11 +133,10 @@ public final class AndroidacyActivity extends FoxActivity {
String configPkg = IntentHelper.getPackageOfConfig(config);
try {
XHooks.checkConfigTargetExists(this, configPkg, config);
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24,
menu -> {
IntentHelper.openConfig(this, config);
return true;
});
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, menu -> {
IntentHelper.openConfig(this, config);
return true;
});
} catch (PackageManager.NameNotFoundException ignored) {
}
}
@ -142,8 +155,7 @@ public final class AndroidacyActivity extends FoxActivity {
webSettings.setAllowContentAccess(false);
// Attempt at fixing CloudFlare captcha.
if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_CONTROL)) {
WebSettingsCompat.setRequestedWithHeaderMode(
webSettings, WebSettingsCompat.REQUESTED_WITH_HEADER_MODE_NO_HEADER);
WebSettingsCompat.setRequestedWithHeaderMode(webSettings, WebSettingsCompat.REQUESTED_WITH_HEADER_MODE_NO_HEADER);
}
// If API level is .= 33, allow setAlgorithmicDarkeningAllowed
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
@ -153,23 +165,19 @@ public final class AndroidacyActivity extends FoxActivity {
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Make website follow app theme
webSettings.setForceDark(MainApplication.getINSTANCE().isLightTheme() ?
WebSettings.FORCE_DARK_OFF : WebSettings.FORCE_DARK_ON);
webSettings.setForceDark(MainApplication.getINSTANCE().isLightTheme() ? WebSettings.FORCE_DARK_OFF : WebSettings.FORCE_DARK_ON);
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
// If api level is < 32, use force dark
WebSettingsCompat.setForceDark(webSettings, MainApplication.getINSTANCE().isLightTheme() ?
WebSettingsCompat.FORCE_DARK_OFF : WebSettingsCompat.FORCE_DARK_ON);
WebSettingsCompat.setForceDark(webSettings, MainApplication.getINSTANCE().isLightTheme() ? WebSettingsCompat.FORCE_DARK_OFF : WebSettingsCompat.FORCE_DARK_ON);
}
}
this.webView.setWebViewClient(new WebViewClientCompat() {
private String pageUrl;
@Override
public boolean shouldOverrideUrlLoading(
@NonNull WebView view, @NonNull WebResourceRequest request) {
public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull WebResourceRequest request) {
// Don't open non Androidacy urls inside WebView
if (request.isForMainFrame() &&
!AndroidacyUtil.isAndroidacyLink(request.getUrl())) {
if (request.isForMainFrame() && !AndroidacyUtil.isAndroidacyLink(request.getUrl())) {
if (downloadMode || backOnResume) return true;
Log.i(TAG, "Exiting WebView " + // hideToken in case isAndroidacyLink fail.
AndroidacyUtil.hideToken(request.getUrl().toString()));
@ -181,13 +189,10 @@ public final class AndroidacyActivity extends FoxActivity {
@Nullable
@Override
public WebResourceResponse shouldInterceptRequest(
WebView view, WebResourceRequest request) {
if (AndroidacyActivity.this.megaIntercept(
this.pageUrl, request.getUrl().toString())) {
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
if (AndroidacyActivity.this.megaIntercept(this.pageUrl, request.getUrl().toString())) {
// Block request as Androidacy doesn't allow duplicate requests
return new WebResourceResponse("text/plain", "UTF-8",
new ByteArrayInputStream(new byte[0]));
return new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream(new byte[0]));
}
return null;
}
@ -205,15 +210,11 @@ public final class AndroidacyActivity extends FoxActivity {
}
private void onReceivedError(String url, int errorCode) {
if ((url.startsWith("https://production-api.androidacy.com/magisk/") ||
url.startsWith("https://staging-api.androidacy.com/magisk/") ||
url.equals(pageUrl)) && (errorCode == 419 || errorCode == 429 || errorCode == 503)) {
Toast.makeText(AndroidacyActivity.this,
"Too many requests!", Toast.LENGTH_LONG).show();
if ((url.startsWith("https://production-api.androidacy.com/magisk/") || url.startsWith("https://staging-api.androidacy.com/magisk/") || url.equals(pageUrl)) && (errorCode == 419 || errorCode == 429 || errorCode == 503)) {
Toast.makeText(AndroidacyActivity.this, "Too many requests!", Toast.LENGTH_LONG).show();
AndroidacyActivity.this.runOnUiThread(AndroidacyActivity.this::onBackPressed);
} else if (url.equals(this.pageUrl)) {
postOnUiThread(() ->
webViewNote.setVisibility(View.VISIBLE));
postOnUiThread(() -> webViewNote.setVisibility(View.VISIBLE));
}
}
@ -223,8 +224,7 @@ public final class AndroidacyActivity extends FoxActivity {
}
@Override
public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request,
@NonNull WebResourceErrorCompat error) {
public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request, @NonNull WebResourceErrorCompat error) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) {
this.onReceivedError(request.getUrl().toString(), error.getErrorCode());
}
@ -232,12 +232,8 @@ public final class AndroidacyActivity extends FoxActivity {
});
this.webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams) {
FoxActivity.getFoxActivity(webView).startActivityForResult(
fileChooserParams.createIntent(), (code, data) ->
filePathCallback.onReceiveValue(
FileChooserParams.parseResult(code, data)));
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
FoxActivity.getFoxActivity(webView).startActivityForResult(fileChooserParams.createIntent(), (code, data) -> filePathCallback.onReceiveValue(FileChooserParams.parseResult(code, data)));
return true;
}
@ -277,62 +273,31 @@ public final class AndroidacyActivity extends FoxActivity {
progressIndicator.setVisibility(View.INVISIBLE);
}
});
this.webView.setDownloadListener((
downloadUrl, userAgent, contentDisposition, mimetype, contentLength) -> {
this.webView.setDownloadListener((downloadUrl, userAgent, contentDisposition, mimetype, contentLength) -> {
if (this.downloadMode || this.isDownloadUrl(downloadUrl)) return;
if (AndroidacyUtil.isAndroidacyLink(downloadUrl) && !this.backOnResume) {
AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI;
if (androidacyWebAPI != null) {
if (!androidacyWebAPI.downloadMode) {
// Native module popup may cause download after consumed action
if (androidacyWebAPI.consumedAction)
return;
if (androidacyWebAPI.consumedAction) return;
// Workaround Androidacy bug
final String moduleId = moduleIdOfUrl(downloadUrl);
if (moduleId != null && !this.isFileUrl(downloadUrl)) {
webView.evaluateJavascript("document.querySelector(" +
"\"#download-form input[name=_token]\").value",
result -> new Thread("Androidacy popup workaround thread") {
@Override
public void run() {
if (androidacyWebAPI.consumedAction) return;
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("moduleId", moduleId);
jsonObject.put("token", AndroidacyRepoData
.getInstance().getToken());
jsonObject.put("_token", result);
String realUrl = Http.doHttpPostRedirect(downloadUrl,
jsonObject.toString(), true);
if (downloadUrl.equals(realUrl)) {
Log.e(TAG, "Failed to resolve URL from " +
downloadUrl);
AndroidacyActivity.this.megaIntercept(
webView.getUrl(), downloadUrl);
return;
}
Log.i(TAG, "Got url: " + realUrl);
androidacyWebAPI.openNativeModuleDialogRaw(realUrl,
moduleId, "", androidacyWebAPI.canInstall());
} catch (IOException | JSONException e) {
Log.e(TAG, "Failed redirect intercept", e);
}
}
}.start());
return;
} else if (this.megaIntercept(webView.getUrl(), downloadUrl))
if (this.megaIntercept(webView.getUrl(), downloadUrl)) {
// Block request as Androidacy doesn't allow duplicate requests
return;
} else if (moduleId != null) {
// Download module
Log.i(TAG, "megaIntercept failure. Forcing onBackPress");
this.onBackPressed();
}
}
androidacyWebAPI.consumedAction = true;
androidacyWebAPI.downloadMode = false;
}
this.backOnResume = true;
Log.i(TAG, "Exiting WebView " +
AndroidacyUtil.hideToken(downloadUrl));
for (String prefix : new String[]{
"https://production-api.androidacy.com/magisk/download/",
"https://staging-api.androidacy.com/magisk/download/"
}) {
Log.i(TAG, "Exiting WebView " + AndroidacyUtil.hideToken(downloadUrl));
for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/magisk/downloads/"}) {
if (downloadUrl.startsWith(prefix)) {
return;
}
@ -345,8 +310,11 @@ public final class AndroidacyActivity extends FoxActivity {
this.webView.addJavascriptInterface(this.androidacyWebAPI, "mmm");
if (compatLevel != 0) androidacyWebAPI.notifyCompatModeRaw(compatLevel);
HashMap<String, String> headers = new HashMap<>();
headers.put("Accept-Language", this.getResources()
.getConfiguration().locale.toLanguageTag());
headers.put("Accept-Language", this.getResources().getConfiguration().locale.toLanguageTag());
if (BuildConfig.DEBUG) {
headers.put("X-Debug", "true");
Log.i(TAG, "Debug mode enabled for webview using URL: " + url + " with headers: " + headers);
}
this.webView.loadUrl(url, headers);
}
@ -372,14 +340,7 @@ public final class AndroidacyActivity extends FoxActivity {
}
private String moduleIdOfUrl(String url) {
for (String prefix : new String[]{
"https://production-api.androidacy.com/magisk/download/",
"https://staging-api.androidacy.com/magisk/download/",
"https://production-api.androidacy.com/magisk/readme/",
"https://staging-api.androidacy.com/magisk/readme/",
"https://prodiuction-api.androidacy.com/magisk/info/",
"https://staging-api.androidacy.com/magisk/info/"
}) { // Make both staging and non staging act the same
for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/downloads/", "https://production-api.androidacy.com/magisk/readme/", "https://staging-api.androidacy.com/magisk/readme/", "https://prodiuction-api.androidacy.com/magisk/info/", "https://staging-api.androidacy.com/magisk/info/"}) { // Make both staging and non staging act the same
int i = url.indexOf('?', prefix.length());
if (i == -1) i = url.length();
if (url.startsWith(prefix)) return url.substring(prefix.length(), i);
@ -400,20 +361,14 @@ public final class AndroidacyActivity extends FoxActivity {
private boolean isFileUrl(String url) {
if (url == null) return false;
for (String prefix : new String[]{
"https://production-api.androidacy.com/magisk/file/",
"https://staging-api.androidacy.com/magisk/file/"
}) { // Make both staging and non staging act the same
for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/downloads/"}) { // Make both staging and non staging act the same
if (url.startsWith(prefix)) return true;
}
return false;
}
private boolean isDownloadUrl(String url) {
for (String prefix : new String[]{
"https://production-api.androidacy.com/magisk/download/",
"https://staging-api.androidacy.com/magisk/download/"
}) { // Make both staging and non staging act the same
for (String prefix : new String[]{"https://production-api.androidacy.com/magisk/downloads/", "https://staging-api.androidacy.com/magisk/downloads/"}) { // Make both staging and non staging act the same
if (url.startsWith(prefix)) return true;
}
return false;
@ -422,19 +377,18 @@ public final class AndroidacyActivity extends FoxActivity {
private boolean megaIntercept(String pageUrl, String fileUrl) {
if (pageUrl == null || fileUrl == null) return false;
if (this.isFileUrl(fileUrl)) {
Log.d(TAG, "megaIntercept(" +
AndroidacyUtil.hideToken(pageUrl) + ", " +
AndroidacyUtil.hideToken(fileUrl) + ")");
Log.d(TAG, "megaIntercept(" + AndroidacyUtil.hideToken(pageUrl) + ", " + AndroidacyUtil.hideToken(fileUrl) + ")");
} else return false;
final AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI;
String moduleId = this.moduleIdOfUrl(fileUrl);
if (moduleId == null) moduleId = this.moduleIdOfUrl(pageUrl);
String moduleId = AndroidacyUtil.getModuleId(fileUrl);
if (moduleId == null) {
Log.d(TAG, "No module id?");
return false;
// Re-open the page
this.webView.loadUrl(pageUrl + "&force_refresh=" + System.currentTimeMillis());
}
androidacyWebAPI.openNativeModuleDialogRaw(fileUrl,
moduleId, "", androidacyWebAPI.canInstall());
String checksum = AndroidacyUtil.getChecksumFromURL(fileUrl);
String moduleTitle = AndroidacyUtil.getModuleTitle(fileUrl);
androidacyWebAPI.openNativeModuleDialogRaw(fileUrl, moduleId, moduleTitle, checksum, androidacyWebAPI.canInstall());
return true;
}
@ -446,21 +400,17 @@ public final class AndroidacyActivity extends FoxActivity {
});
byte[] module;
try {
module = Http.doHttpGet(url, (downloaded, total, done) ->
progressIndicator.setProgressCompat((downloaded * 100) / total, true));
module = Http.doHttpGet(url, (downloaded, total, done) -> progressIndicator.setProgressCompat((downloaded * 100) / total, true));
try (FileOutputStream fileOutputStream = new FileOutputStream(this.moduleFile)) {
fileOutputStream.write(module);
}
} finally {
//noinspection UnusedAssignment
module = null;
this.runOnUiThread(() ->
progressIndicator.setVisibility(View.INVISIBLE));
this.runOnUiThread(() -> progressIndicator.setVisibility(View.INVISIBLE));
}
this.backOnResume = true;
this.downloadMode = false;
return FileProvider.getUriForFile(this,
this.getPackageName() + ".file-provider",
this.moduleFile);
return FileProvider.getUriForFile(this, this.getPackageName() + ".file-provider", this.moduleFile);
}
}

@ -1,11 +1,13 @@
package com.fox2code.mmm.androidacy;
import android.content.SharedPreferences;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.manager.ModuleInfo;
@ -15,7 +17,8 @@ import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.HttpException;
import com.fox2code.mmm.utils.PropUtils;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.Shell;
import org.json.JSONArray;
import org.json.JSONException;
@ -23,10 +26,12 @@ import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import okhttp3.HttpUrl;
@ -41,11 +46,14 @@ public final class AndroidacyRepoData extends RepoData {
OK_HTTP_URL_BUILDER.build();
}
@SuppressWarnings("unused")
public final String ClientID = BuildConfig.ANDROIDACY_CLIENT_ID;
private final boolean testMode;
private final String host;
public String token = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).getString("pref_androidacy_api_token", null);
// Avoid spamming requests to Androidacy
private long androidacyBlockade = 0;
private String token = this.cachedPreferences.getString("pref_androidacy_api_token", null);
public AndroidacyRepoData(File cacheRoot, SharedPreferences cachedPreferences, boolean testMode) {
super(testMode ? RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT : RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT, cacheRoot, cachedPreferences);
@ -75,15 +83,68 @@ public final class AndroidacyRepoData extends RepoData {
return url;
}
public boolean isValidToken(String token) throws IOException {
// Generates a unique device ID. This is used to identify the device in the API for rate
// limiting and fraud detection.
public static String generateDeviceId() throws NoSuchAlgorithmException {
// Try to get the device ID from the shared preferences
SharedPreferences sharedPreferences = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0);
String deviceIdPref = sharedPreferences.getString("device_id", null);
if (deviceIdPref != null) {
return deviceIdPref;
} else {
// AAAA we're fingerprintiiiiing
// Really not that scary - just hashes some device info. We can't even get the info
// we originally hashed, so it's not like we can use it to track you.
String deviceId = null;
// Get ro.serialno if it exists
// First, we need to get an su shell
Shell.Result result = Shell.cmd("getprop ro.serialno").exec();
// Check if the command was successful
if (result.isSuccess()) {
// Get the output
String output = result.getOut().get(0);
// Check if the output is valid
if (output != null && !output.isEmpty()) {
deviceId = output;
}
}
// Now, get device model, manufacturer, and Android version
String deviceModel = android.os.Build.MODEL;
String deviceManufacturer = android.os.Build.MANUFACTURER;
String androidVersion = android.os.Build.VERSION.RELEASE;
// Append it all together
deviceId += deviceModel + deviceManufacturer + androidVersion;
// Hash it
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(deviceId.getBytes());
// Convert it to a hex string
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
// Save it to shared preferences
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("device_id", hexString.toString());
editor.apply();
// Return it
return hexString.toString();
}
}
public boolean isValidToken(String token) throws IOException, NoSuchAlgorithmException {
String deviceId = generateDeviceId();
try {
Http.doHttpGet("https://" + this.host + "/auth/me?token=" + token, false);
Http.doHttpGet("https://" + this.host + "/auth/me?token=" + token + "&device_id=" + deviceId, false);
} catch (HttpException e) {
if (e.getErrorCode() == 401) {
Log.w(TAG, "Invalid token, resetting...");
// Remove saved preference
SharedPreferences.Editor editor = this.cachedPreferences.edit();
editor.remove("androidacy_api_token");
SharedPreferences.Editor editor = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit();
editor.remove("pref_androidacy_api_token");
editor.apply();
return false;
}
@ -94,7 +155,14 @@ public final class AndroidacyRepoData extends RepoData {
}
@Override
protected boolean prepare() {
protected boolean prepare() throws NoSuchAlgorithmException {
// If ANDROIDACY_CLIENT_ID is not set or is empty, disable this repo and return
if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) {
SharedPreferences.Editor editor = this.cachedPreferences.edit();
editor.putBoolean("pref_androidacy_repo_enabled", false);
editor.apply();
return false;
}
if (Http.needCaptchaAndroidacy()) return false;
// Implementation details discussed on telegram
// First, ping the server to check if it's alive
@ -103,12 +171,18 @@ public final class AndroidacyRepoData extends RepoData {
} catch (Exception e) {
Log.e(TAG, "Failed to ping server", e);
// Inform user
/*if (!HttpException.shouldTimeout(e)) {
UiThreadHandler.run(() -> Toast.makeText(MainApplication.getINSTANCE(),
R.string.androidacy_server_down, Toast.LENGTH_SHORT).show());
}*/
Looper.prepare();
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(MainApplication.getINSTANCE().getBaseContext());
builder.setTitle("Androidacy Server Down");
builder.setMessage("The Androidacy server is down. Unfortunately, this means that you" +
" will not be able to download or view modules from the Androidacy repository" +
". Please try again later.");
builder.setPositiveButton("OK", (dialog, which) -> dialog.dismiss());
builder.show();
Looper.loop();
return false;
}
String deviceId = generateDeviceId();
long time = System.currentTimeMillis();
if (this.androidacyBlockade > time) return false;
this.androidacyBlockade = time + 30_000L;
@ -117,8 +191,13 @@ public final class AndroidacyRepoData extends RepoData {
this.token = this.cachedPreferences.getString("pref_androidacy_api_token", null);
if (this.token != null && !this.isValidToken(this.token)) {
this.token = null;
} else {
Log.i(TAG, "Using cached token");
}
} else if (!this.isValidToken(this.token)) {
if (BuildConfig.DEBUG) {
throw new IllegalStateException("Invalid token: " + this.token);
}
this.token = null;
}
} catch (IOException e) {
@ -130,9 +209,9 @@ public final class AndroidacyRepoData extends RepoData {
}
if (token == null) {
try {
Log.i(TAG, "Refreshing token...");
// POST request to https://production-api.androidacy.com/auth/register
token = new String(Http.doHttpPost("https://" + this.host + "/auth/register", "foxmmm=true", false), StandardCharsets.UTF_8);
Log.i(TAG, "Requesting new token...");
// POST json request to https://production-api.androidacy.com/auth/register
token = new String(Http.doHttpPost("https://" + this.host + "/auth/register", "{\"device_id\":\"" + deviceId + "\"}", false));
// Parse token
try {
JSONObject jsonObject = new JSONObject(token);
@ -151,7 +230,9 @@ public final class AndroidacyRepoData extends RepoData {
return false;
}
// Save token to shared preference
MainApplication.getSharedPreferences().edit().putString("pref_androidacy_api_token", token).apply();
SharedPreferences.Editor editor = MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit();
editor.putString("pref_androidacy_api_token", token);
editor.apply();
} catch (Exception e) {
if (HttpException.shouldTimeout(e)) {
Log.e(TAG, "We are being rate limited!", e);
@ -167,7 +248,7 @@ public final class AndroidacyRepoData extends RepoData {
}
@Override
protected List<RepoModule> populate(JSONObject jsonObject) throws JSONException {
protected List<RepoModule> populate(JSONObject jsonObject) throws JSONException, NoSuchAlgorithmException {
if (!jsonObject.getString("status").equals("success"))
throw new JSONException("Response is not a success!");
String name = jsonObject.optString("name", "Androidacy Modules Repo");
@ -280,11 +361,12 @@ public final class AndroidacyRepoData extends RepoData {
}
@Override
public String getUrl() {
return this.token == null ? this.url : this.url + "?token=" + this.token;
public String getUrl() throws NoSuchAlgorithmException {
return this.token == null ? this.url :
this.url + "?token=" + this.token + "&v=" + BuildConfig.VERSION_CODE + "&c=" + BuildConfig.VERSION_NAME + "&device_id=" + generateDeviceId();
}
private String injectToken(String url) {
private String injectToken(String url) throws NoSuchAlgorithmException {
// Do not inject token for non Androidacy urls
if (!AndroidacyUtil.isAndroidacyLink(url)) return url;
if (this.testMode) {
@ -299,6 +381,7 @@ public final class AndroidacyRepoData extends RepoData {
}
}
String token = "token=" + this.token;
String deviceId = "device_id=" + generateDeviceId();
if (!url.contains(token)) {
if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
return url + '&' + token;
@ -306,6 +389,13 @@ public final class AndroidacyRepoData extends RepoData {
return url + '?' + token;
}
}
if (!url.contains(deviceId)) {
if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
return url + '&' + deviceId;
} else {
return url + '?' + deviceId;
}
}
return url;
}
@ -315,10 +405,6 @@ public final class AndroidacyRepoData extends RepoData {
return this.testMode ? super.getName() + " (Test Mode)" : super.getName();
}
String getToken() {
return this.token;
}
public void setToken(String token) {
if (Http.hasWebView()) {
this.token = token;

@ -5,6 +5,8 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.fox2code.mmm.BuildConfig;
public class AndroidacyUtil {
public static final String REFERRER = "utm_source=FoxMMM&utm_medium=app";
@ -50,4 +52,58 @@ public class AndroidacyUtil {
"<token>" + url.substring(i2);
}
}
public static String getModuleId(String moduleUrl) {
// Get the &module= part
int i = moduleUrl.indexOf("&module=");
String moduleId;
// Match until next & or end
if (i != -1) {
int j = moduleUrl.indexOf('&', i + 1);
if (j == -1) {
moduleId = moduleUrl.substring(i + 8);
} else {
moduleId = moduleUrl.substring(i + 8, j);
}
// URL decode
moduleId = Uri.decode(moduleId);
// Strip non alphanumeric
moduleId = moduleId.replaceAll("[^a-zA-Z0-9]", "");
return moduleId;
}
if (BuildConfig.DEBUG) {
throw new IllegalArgumentException("Invalid module url: " + moduleUrl);
}
return null;
}
public static String getModuleTitle(String moduleUrl) {
// Get the &title= part
int i = moduleUrl.indexOf("&moduleTitle=");
// Match until next & or end
if (i != -1) {
int j = moduleUrl.indexOf('&', i + 1);
if (j == -1) {
return Uri.decode(moduleUrl.substring(i + 13));
} else {
return Uri.decode(moduleUrl.substring(i + 13, j));
}
}
return null;
}
public static String getChecksumFromURL(String moduleUrl) {
// Get the &version= part
int i = moduleUrl.indexOf("&checksum=");
// Match until next & or end
if (i != -1) {
int j = moduleUrl.indexOf('&', i + 1);
if (j == -1) {
return moduleUrl.substring(i + 10);
} else {
return moduleUrl.substring(i + 10, j);
}
}
return null;
}
}

@ -62,9 +62,11 @@ public class AndroidacyWebAPI {
this.downloadMode = false;
}
void openNativeModuleDialogRaw(String moduleUrl, String installTitle,
void openNativeModuleDialogRaw(String moduleUrl, String moduleId, String installTitle,
String checksum, boolean canInstall) {
Log.d(TAG, "ModuleDialog, downloadUrl: " + AndroidacyUtil.hideToken(moduleUrl));
Log.d(TAG, "ModuleDialog, downloadUrl: " + AndroidacyUtil.hideToken(moduleUrl) +
", moduleId: " + moduleId + ", installTitle: " + installTitle +
", checksum: " + checksum + ", canInstall: " + canInstall);
this.downloadMode = false;
RepoModule repoModule = AndroidacyRepoData
.getInstance().moduleHashMap.get(installTitle);
@ -78,7 +80,8 @@ public class AndroidacyWebAPI {
description = this.activity.getString(R.string.no_desc_found);
}
} else {
title = PropUtils.makeNameFromId(installTitle);
// URL Decode installTitle
title = installTitle;
String checkSumType = Hashes.checkSumName(checksum);
if (checkSumType == null) {
description = "Checksum: " + ((
@ -249,6 +252,8 @@ public class AndroidacyWebAPI {
this.forceQuitRaw("Androidacy didn't provided a valid checksum");
return;
}
// moduleId is the module parameter in the url
String moduleId = AndroidacyUtil.getModuleId(moduleUrl);
// Let's handle download mode ourself if not implemented
if (this.effectiveCompatMode < 1) {
if (!this.canInstall()) {
@ -256,7 +261,7 @@ public class AndroidacyWebAPI {
this.activity.runOnUiThread(() ->
this.activity.webView.loadUrl(moduleUrl));
} else {
this.openNativeModuleDialogRaw(moduleUrl, installTitle, checksum, true);
this.openNativeModuleDialogRaw(moduleUrl, moduleId, installTitle, checksum, true);
}
} else {
RepoModule repoModule = AndroidacyRepoData
@ -293,7 +298,9 @@ public class AndroidacyWebAPI {
this.forceQuitRaw("Androidacy didn't provided a valid checksum");
return;
}
this.openNativeModuleDialogRaw(moduleUrl, moduleId, checksum, this.canInstall());
// Get moduleTitle from url
String moduleTitle = AndroidacyUtil.getModuleTitle(moduleUrl);
this.openNativeModuleDialogRaw(moduleUrl, moduleId, moduleTitle, checksum, this.canInstall());
}
/**

@ -10,6 +10,7 @@ import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
public final class CustomRepoData extends RepoData {
boolean loadedExternal;
@ -30,12 +31,7 @@ public final class CustomRepoData extends RepoData {
this.id : this.override;
}
@Override
public boolean isLimited() {
return true;
}
public void quickPrePopulate() throws IOException, JSONException {
public void quickPrePopulate() throws IOException, JSONException, NoSuchAlgorithmException {
JSONObject jsonObject = new JSONObject(
new String(Http.doHttpGet(this.getUrl(),
false), StandardCharsets.UTF_8));

@ -3,6 +3,8 @@ package com.fox2code.mmm.repo;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.utils.PropUtils;
@ -65,6 +67,8 @@ public class CustomRepoManager {
CustomRepoData customRepoData = (CustomRepoData)
this.repoManager.addOrGet(repo);
customRepoData.override = "custom_repo_" + i;
// Set the enabled state to true
customRepoData.setEnabled(true);
customRepoData.updateEnabledState();
return customRepoData;
}

@ -21,13 +21,13 @@ import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
public class RepoData extends XRepo {
private static final String TAG = "RepoData";
private final Object populateLock = new Object();
public final String url;
public final String id;
@ -54,7 +54,10 @@ public class RepoData extends XRepo {
.getBoolean("pref_" + this.id + "_enabled", this.isEnabledByDefault());
this.defaultWebsite = "https://" + Uri.parse(url).getHost() + "/";
if (!this.cacheRoot.isDirectory()) {
this.cacheRoot.mkdirs();
boolean mkdirs = this.cacheRoot.mkdirs();
if (!mkdirs) {
throw new RuntimeException("Failed to create cache directory");
}
} else {
if (this.metaDataCache.exists()) {
this.lastUpdate = metaDataCache.lastModified();
@ -70,17 +73,20 @@ public class RepoData extends XRepo {
}
}
} catch (Exception e) {
this.metaDataCache.delete();
boolean delete = this.metaDataCache.delete();
if (!delete) {
throw new RuntimeException("Failed to delete invalid cache file");
}
}
}
}
}
protected boolean prepare() {
protected boolean prepare() throws NoSuchAlgorithmException {
return true;
}
protected List<RepoModule> populate(JSONObject jsonObject) throws JSONException {
protected List<RepoModule> populate(JSONObject jsonObject) throws JSONException, NoSuchAlgorithmException {
List<RepoModule> newModules = new ArrayList<>();
synchronized (this.populateLock) {
String name = jsonObject.getString("name").trim();
@ -140,7 +146,10 @@ public class RepoData extends XRepo {
while (moduleInfoIterator.hasNext()) {
RepoModule repoModule = moduleInfoIterator.next();
if (!repoModule.processed) {
new File(this.cacheRoot, repoModule.id + ".prop").delete();
boolean delete = new File(this.cacheRoot, repoModule.id + ".prop").delete();
if (!delete) {
throw new RuntimeException("Failed to delete module metadata");
}
moduleInfoIterator.remove();
} else {
repoModule.moduleInfo.verify();
@ -179,7 +188,10 @@ public class RepoData extends XRepo {
}
return true;
} catch (Exception ignored) {
file.delete();
boolean delete = file.delete();
if (!delete) {
throw new RuntimeException("Failed to delete invalid metadata file");
}
}
}
repoModule.moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID;
@ -204,14 +216,10 @@ public class RepoData extends XRepo {
.getBoolean("pref_" + this.getPreferenceId() + "_enabled", this.isEnabledByDefault());
}
public String getUrl() {
public String getUrl() throws NoSuchAlgorithmException {
return this.url;
}
public boolean isLimited() {
return false;
}
public String getPreferenceId() {
return this.id;
}

@ -19,6 +19,9 @@ import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.SyncManager;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
@ -27,37 +30,74 @@ import java.util.LinkedHashSet;
import java.util.List;
public final class RepoManager extends SyncManager {
private static final String TAG = "RepoManager";
private static final String MAGISK_REPO_MANAGER =
"https://magisk-modules-repo.github.io/submission/modules.json";
public static final String MAGISK_REPO =
"https://raw.githubusercontent.com/Magisk-Modules-Repo/submission/modules/modules.json";
public static final String MAGISK_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Repo";
public static final String MAGISK_ALT_REPO =
"https://raw.githubusercontent.com/Magisk-Modules-Alt-Repo/json/main/modules.json";
public static final String MAGISK_ALT_REPO_HOMEPAGE =
"https://github.com/Magisk-Modules-Alt-Repo";
public static final String MAGISK_ALT_REPO_JSDELIVR =
"https://cdn.jsdelivr.net/gh/Magisk-Modules-Alt-Repo/json@main/modules.json";
public static final String ANDROIDACY_MAGISK_REPO_ENDPOINT =
"https://production-api.androidacy.com/magisk/repo";
public static final String ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT =
"https://staging-api.androidacy.com/magisk/repo";
public static final String ANDROIDACY_MAGISK_REPO_HOMEPAGE =
"https://www.androidacy.com/modules-repo";
public static final String DG_MAGISK_REPO =
"https://repo.dergoogler.com/modules.json";
public static final String DG_MAGISK_REPO_GITHUB =
"https://googlers-magisk-repo.github.io/modules.json";
public static final String DG_MAGISK_REPO_GITHUB_RAW =
"https://raw.githubusercontent.com/Googlers-Repo/googlers-repo.github.io/master/modules.json";
private static final String TAG = "RepoManager";
private static final String MAGISK_REPO_MANAGER =
"https://magisk-modules-repo.github.io/submission/modules.json";
private static final Object lock = new Object();
private static final double STEP1 = 0.1D;
private static final double STEP2 = 0.8D;
private static final double STEP3 = 0.1D;
private static volatile RepoManager INSTANCE;
private final MainApplication mainApplication;
private final LinkedHashMap<String, RepoData> repoData;
private final HashMap<String, RepoModule> modules;
private final AndroidacyRepoData androidacyRepoData;
private final CustomRepoManager customRepoManager;
private boolean hasInternet;
private boolean repoLastError = false;
private boolean initialized;
public String repoLastErrorName = null;
private RepoManager(MainApplication mainApplication) {
INSTANCE = this; // Set early fox XHooks
this.initialized = false;
this.mainApplication = mainApplication;
this.repoData = new LinkedHashMap<>();
this.modules = new HashMap<>();
// We do not have repo list config yet.
RepoData altRepo = this.addRepoData(
MAGISK_ALT_REPO, "Magisk Modules Alt Repo");
altRepo.defaultWebsite = RepoManager.MAGISK_ALT_REPO_HOMEPAGE;
altRepo.defaultSubmitModule =
"https://github.com/Magisk-Modules-Alt-Repo/submission/issues";
RepoData dgRepo = this.addRepoData(
DG_MAGISK_REPO_GITHUB_RAW, "Googlers Magisk Repo");
dgRepo.defaultWebsite = "https://dergoogler.com/repo";
this.androidacyRepoData = this.addAndroidacyRepoData();
this.customRepoManager = new CustomRepoManager(mainApplication, this);
XHooks.onRepoManagerInitialize();
// Populate default cache
boolean x = false;
for (RepoData repoData : this.repoData.values()) {
if (repoData == this.androidacyRepoData) {
if (x) return;
x = true;
}
this.populateDefaultCache(repoData);
}
this.initialized = true;
}
public static RepoManager getINSTANCE() {
if (INSTANCE == null || !INSTANCE.initialized) {
@ -93,44 +133,50 @@ public final class RepoManager extends SyncManager {
return INSTANCE;
}
private final MainApplication mainApplication;
private final LinkedHashMap<String, RepoData> repoData;
private final HashMap<String, RepoModule> modules;
private final AndroidacyRepoData androidacyRepoData;
private final CustomRepoManager customRepoManager;
private boolean initialized;
public static String internalIdOfUrl(String url) {
switch (url) {
case MAGISK_ALT_REPO:
case MAGISK_ALT_REPO_JSDELIVR:
return "magisk_alt_repo";
case ANDROIDACY_MAGISK_REPO_ENDPOINT:
case ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT:
return "androidacy_repo";
case DG_MAGISK_REPO:
case DG_MAGISK_REPO_GITHUB:
case DG_MAGISK_REPO_GITHUB_RAW:
return "dg_magisk_repo";
default:
return "repo_" + Hashes.hashSha1(
url.getBytes(StandardCharsets.UTF_8));
}
}
private RepoManager(MainApplication mainApplication) {
INSTANCE = this; // Set early fox XHooks
this.initialized = false;
this.mainApplication = mainApplication;
this.repoData = new LinkedHashMap<>();
this.modules = new HashMap<>();
// We do not have repo list config yet.
RepoData altRepo = this.addRepoData(
MAGISK_ALT_REPO, "Magisk Modules Alt Repo");
altRepo.defaultWebsite = RepoManager.MAGISK_ALT_REPO_HOMEPAGE;
altRepo.defaultSubmitModule =
"https://github.com/Magisk-Modules-Alt-Repo/submission/issues";
RepoData dgRepo = this.addRepoData(
DG_MAGISK_REPO_GITHUB_RAW, "Googlers Magisk Repo");
dgRepo.defaultWebsite = "https://dergoogler.com/repo";
this.androidacyRepoData = this.addAndroidacyRepoData();
this.customRepoManager = new CustomRepoManager(mainApplication, this);
XHooks.onRepoManagerInitialize();
// Populate default cache
boolean x = false;
for (RepoData repoData:this.repoData.values()) {
if (repoData == this.androidacyRepoData) {
if (x) return; x = true;
}
this.populateDefaultCache(repoData);
static boolean isBuiltInRepo(String repo) {
switch (repo) {
case RepoManager.MAGISK_ALT_REPO:
case RepoManager.MAGISK_ALT_REPO_JSDELIVR:
case RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT:
case RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT:
case RepoManager.DG_MAGISK_REPO:
case RepoManager.DG_MAGISK_REPO_GITHUB:
case RepoManager.DG_MAGISK_REPO_GITHUB_RAW:
return true;
}
this.initialized = true;
return false;
}
/**
* Safe way to do {@code RepoManager.getInstance().androidacyRepoData.isEnabled()}
* without initializing RepoManager
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isAndroidacyRepoEnabled() {
return INSTANCE != null && INSTANCE.androidacyRepoData != null &&
INSTANCE.androidacyRepoData.isEnabled();
}
private void populateDefaultCache(RepoData repoData) {
for (RepoModule repoModule:repoData.moduleHashMap.values()) {
for (RepoModule repoModule : repoData.moduleHashMap.values()) {
if (!repoModule.moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
RepoModule registeredRepoModule = this.modules.get(repoModule.id);
if (registeredRepoModule == null) {
@ -181,14 +227,9 @@ public final class RepoManager extends SyncManager {
return repoData;
}
private boolean repoLastResult = true;
private static final double STEP1 = 0.1D;
private static final double STEP2 = 0.8D;
private static final double STEP3 = 0.1D;
protected void scanInternal(@NonNull UpdateListener updateListener) {
NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug();
// First, check if we have internet connection
noodleDebug.push("Downloading indexes");
this.modules.clear();
updateListener.update(0D);
@ -215,7 +256,7 @@ public final class RepoManager extends SyncManager {
noodleDebug.replace(repoData.getName());
Log.d(TAG, "Registering " + repoData.getName());
noodleDebug.push("");
for (RepoModule repoModule:repoModules) {
for (RepoModule repoModule : repoModules) {
noodleDebug.replace(repoModule.id);
try {
if (repoModule.propUrl != null &&
@ -243,7 +284,7 @@ public final class RepoManager extends SyncManager {
updateListener.update(STEP1 + (STEP2 / moduleToUpdate * updatedModules));
}
noodleDebug.pop();
for (RepoModule repoModule:repoUpdaters[i].toApply()) {
for (RepoModule repoModule : repoUpdaters[i].toApply()) {
if ((repoModule.moduleInfo.flags & ModuleInfo.FLAG_METADATA_INVALID) == 0) {
RepoModule registeredRepoModule = this.modules.get(repoModule.id);
if (registeredRepoModule == null) {
@ -258,21 +299,45 @@ public final class RepoManager extends SyncManager {
noodleDebug.pop();
noodleDebug.replace("Finishing update");
noodleDebug.push("");
boolean hasInternet = false;
for (int i = 0; i < repoDatas.length; i++) {
noodleDebug.replace(repoUpdaters[i].repoData.getName());
hasInternet |= repoUpdaters[i].finish();
updateListener.update(STEP1 + STEP2 + (STEP3 / repoDatas.length * (i + 1)));
this.hasInternet = false;
// Check if we have internet connection
// Attempt to contact connectivitycheck.gstatic.com/generate_204
// If we can't, we don't have internet connection
try {
HttpURLConnection urlConnection = (HttpURLConnection) new URL(
"https://connectivitycheck.gstatic.com/generate_204").openConnection();
urlConnection.setInstanceFollowRedirects(false);
urlConnection.setConnectTimeout(1000);
urlConnection.setReadTimeout(1000);
urlConnection.setUseCaches(false);
urlConnection.getInputStream().close();
if (urlConnection.getResponseCode() == 204 &&
urlConnection.getContentLength() == 0) {
this.hasInternet = true;
}
} catch (IOException e) {
Log.e(TAG, "Failed to check internet connection", e);
}
noodleDebug.pop();
if (hasInternet) {
for (int i = 0; i < repoDatas.length; i++) {
noodleDebug.replace(repoUpdaters[i].repoData.getName());
this.repoLastError = !repoUpdaters[i].finish();
if (this.repoLastError) {
Log.e(TAG, "Failed to update " + repoUpdaters[i].repoData.getName());
this.repoLastErrorName = repoUpdaters[i].repoData.getName();
}
updateListener.update(STEP1 + STEP2 + (STEP3 / repoDatas.length * (i + 1)));
}
}
noodleDebug.pop();
Log.i(TAG, "Got " + this.modules.size() + " modules!");
updateListener.update(1D);
this.repoLastResult = hasInternet;
noodleDebug.pop(); // pop "Finishing update"
}
public void updateEnabledStates() {
for (RepoData repoData:this.repoData.values()) {
for (RepoData repoData : this.repoData.values()) {
boolean wasEnabled = repoData.isEnabled();
repoData.updateEnabledState();
if (!wasEnabled && repoData.isEnabled()) {
@ -287,39 +352,7 @@ public final class RepoManager extends SyncManager {
}
public boolean hasConnectivity() {
return this.repoLastResult;
}
public static String internalIdOfUrl(String url) {
switch (url) {
case MAGISK_ALT_REPO:
case MAGISK_ALT_REPO_JSDELIVR:
return "magisk_alt_repo";
case ANDROIDACY_MAGISK_REPO_ENDPOINT:
case ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT:
return "androidacy_repo";
case DG_MAGISK_REPO:
case DG_MAGISK_REPO_GITHUB:
case DG_MAGISK_REPO_GITHUB_RAW:
return "dg_magisk_repo";
default:
return "repo_" + Hashes.hashSha1(
url.getBytes(StandardCharsets.UTF_8));
}
}
static boolean isBuiltInRepo(String repo) {
switch (repo) {
case RepoManager.MAGISK_ALT_REPO:
case RepoManager.MAGISK_ALT_REPO_JSDELIVR:
case RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT:
case RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT:
case RepoManager.DG_MAGISK_REPO:
case RepoManager.DG_MAGISK_REPO_GITHUB:
case RepoManager.DG_MAGISK_REPO_GITHUB_RAW:
return true;
}
return false;
return this.hasInternet;
}
private RepoData addRepoData(String url, String fallBackName) {
@ -374,12 +407,7 @@ public final class RepoManager extends SyncManager {
return new LinkedHashSet<>(this.repoData.values());
}
/**
* Safe way to do {@code RepoManager.getInstance().androidacyRepoData.isEnabled()}
* without initializing RepoManager
*/
public static boolean isAndroidacyRepoEnabled() {
return INSTANCE != null && INSTANCE.androidacyRepoData != null &&
INSTANCE.androidacyRepoData.isEnabled();
public boolean isLastUpdateSuccess() {
return this.repoLastError;
}
}

@ -1,14 +1,25 @@
package com.fox2code.mmm.repo;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.fox2code.mmm.MainActivity;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.HttpException;
import com.google.android.material.snackbar.Snackbar;
import org.jetbrains.annotations.Contract;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@ -40,6 +51,13 @@ public class RepoUpdater {
return 0;
}
this.indexRaw = Http.doHttpGet(this.repoData.getUrl(), false);
// Ensure it's a valid json and response code is 200
if (Arrays.hashCode(this.indexRaw) == 0) {
this.indexRaw = null;
this.toUpdate = Collections.emptyList();
this.toApply = this.repoData.moduleHashMap.values();
return 0;
}
this.toUpdate = this.repoData.populate(new JSONObject(
new String(this.indexRaw, StandardCharsets.UTF_8)));
// Since we reuse instances this should work
@ -66,6 +84,10 @@ public class RepoUpdater {
public boolean finish() {
final boolean success = this.indexRaw != null;
// If repo is not enabled we don't need to do anything, just return true
if (!this.repoData.isEnabled()) {
return true;
}
if (this.indexRaw != null) {
try {
Files.write(this.repoData.metaDataCache, this.indexRaw);

@ -9,10 +9,12 @@ import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
import android.widget.AutoCompleteTextView;
@ -65,6 +67,7 @@ import com.topjohnwu.superuser.internal.UiThreadHandler;
import org.json.JSONException;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Objects;
import java.util.Random;
@ -262,12 +265,39 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
Preference debugNotification = findPreference("pref_background_update_check_debug");
debugNotification.setEnabled(MainApplication.isBackgroundUpdateCheckEnabled());
debugNotification.setVisible(MainApplication.isDeveloper() && !MainApplication.isWrapped());
debugNotification.setVisible(MainApplication.isDeveloper() && !MainApplication.isWrapped());
debugNotification.setOnPreferenceClickListener(preference -> {
BackgroundUpdateChecker.postNotification(this.requireContext(), new Random().nextInt(4) + 2);
return true;
});
Preference backgroundUpdateCheck = findPreference("pref_background_update_check");
backgroundUpdateCheck.setVisible(!MainApplication.isWrapped());
// Make uncheckable if POST_NOTIFICATIONS permission is not granted
if (!MainApplication.isNotificationPermissionGranted()) {
// Instead of disabling the preference, we make it uncheckable and when the user
// clicks on it, we show a dialog explaining why the permission is needed
backgroundUpdateCheck.setOnPreferenceClickListener(preference -> {
// set the box to unchecked
((SwitchPreferenceCompat) backgroundUpdateCheck).setChecked(false);
// ensure that the preference is false
MainApplication.getSharedPreferences().edit().putBoolean("pref_background_update_check", false).apply();
new MaterialAlertDialogBuilder(this.requireContext())
.setTitle(R.string.permission_notification_title)
.setMessage(R.string.permission_notification_message)
.setPositiveButton(R.string.ok, (dialog, which) -> {
// Open the app settings
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", this.requireContext().getPackageName(), null);
intent.setData(uri);
this.startActivity(intent);
})
.setNegativeButton(R.string.cancel, (dialog, which) -> {})
.show();
return true;
});
backgroundUpdateCheck.setSummary(R.string.background_update_check_permission_required);
}
backgroundUpdateCheck.setOnPreferenceChangeListener((preference, newValue) -> {
boolean enabled = Boolean.parseBoolean(String.valueOf(newValue));
debugNotification.setEnabled(enabled);
@ -422,6 +452,8 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
@SuppressLint({"RestrictedApi", "UnspecifiedImmutableFlag"})
public void onCreatePreferencesAndroidacy() {
// Bind the pref_show_captcha_webview to captchaWebview('https://production-api.androidacy.com/')
// Also require dev modeowCaptchaWebview.setVisible(false);
Preference androidacyTestMode = Objects.requireNonNull(findPreference("pref_androidacy_test_mode"));
if (!MainApplication.isDeveloper()) {
androidacyTestMode.setVisible(false);
@ -497,17 +529,44 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
return true;
});
}
String[] originalApiKeyRef = new String[]{
MainApplication.getSharedPreferences().getString("pref_androidacy_api_token", "")};
// Create the pref_androidacy_repo_api_key text input with validation
EditTextPreference prefAndroidacyRepoApiKey = findPreference("pref_androidacy_repo_api_key");
assert prefAndroidacyRepoApiKey != null;
// Disable toggling the pref_androidacy_repo_enabled on builds without an
// ANDROIDACY_CLIENT_ID or where the ANDROIDACY_CLIENT_ID is empty
Preference androidacyRepoEnabled = Objects.requireNonNull(findPreference("pref_androidacy_repo_enabled"));
if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) {
androidacyRepoEnabled.setOnPreferenceClickListener(preference -> {
new MaterialAlertDialogBuilder(this.requireContext())
.setTitle(R.string.androidacy_repo_disabled)
.setMessage(R.string.androidacy_repo_disabled_message)
.setPositiveButton(R.string.download_full_app, (dialog, which) -> {
// User clicked OK button. Open GitHub releases page
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(
"https://github.com/Fox2Code/FoxMagiskModuleManager/releases"));
startActivity(browserIntent);
})
.show();
// Revert the switch to off
SwitchPreferenceCompat switchPreferenceCompat = (SwitchPreferenceCompat) androidacyRepoEnabled;
switchPreferenceCompat.setChecked(false);
// Save the preference
MainApplication.getSharedPreferences().edit().putBoolean("pref_androidacy_repo_enabled", false).apply();
return false;
});
}
String[] originalApiKeyRef = new String[]{MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).getString("pref_androidacy_api_token", null)};
// Get the dummy pref_androidacy_repo_api_token EditTextPreference
EditTextPreference prefAndroidacyRepoApiKey = Objects.requireNonNull(findPreference("pref_androidacy_api_token"));
prefAndroidacyRepoApiKey.setTitle(R.string.api_key);
prefAndroidacyRepoApiKey.setSummary(R.string.api_key_summary);
prefAndroidacyRepoApiKey.setDialogTitle(R.string.api_key);
prefAndroidacyRepoApiKey.setDefaultValue(originalApiKeyRef[0]);
// Set the value to the current value
prefAndroidacyRepoApiKey.setText(originalApiKeyRef[0]);
prefAndroidacyRepoApiKey.setOnBindEditTextListener(editText -> {
editText.setSingleLine();
// Make the single line wrap
editText.setHorizontallyScrolling(false);
// Set the height to the height of 2 lines
editText.setHeight(editText.getLineHeight() * 3);
// Set the height to the maximum required to fit the text
editText.setMaxLines(Integer.MAX_VALUE);
// Make ok button say "Save"
editText.setImeOptions(EditorInfo.IME_ACTION_DONE);
});
@ -528,41 +587,98 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
new Thread(() -> {
// If key is empty, just remove it and change the text of the snack bar
if (apiKey.isEmpty()) {
MainApplication.getSharedPreferences().edit().remove(
"pref_androidacy_repo_api_key").apply();
new Handler(Looper.getMainLooper()).post(() -> Snackbar.make(requireView(),
R.string.api_key_removed, Snackbar.LENGTH_SHORT).show());
MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit().remove("pref_androidacy_api_token").apply();
new Handler(Looper.getMainLooper()).post(() -> {
Snackbar.make(requireView(), R.string.api_key_removed, Snackbar.LENGTH_SHORT).show();
// Show dialog to restart app with ok button
new MaterialAlertDialogBuilder(this.requireContext())
.setTitle(R.string.restart)
.setMessage(R.string.api_key_restart)
.setNeutralButton(android.R.string.ok, (dialog, which) -> {
// User clicked OK button
Intent mStartActivity = new Intent(requireContext(), MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
int mPendingIntentId = 123456;
// If < 23, FLAG_IMMUTABLE is not available
PendingIntent mPendingIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId,
mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} else {
mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId,
mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT);
}
AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Restarting app to save token preference: " + newValue);
}
System.exit(0); // Exit app process
})
.show();
});
} else {
// If key < 64 chars, it's not valid
if (apiKey.length() < 64) {
new Handler(Looper.getMainLooper()).post(() -> {
Snackbar.make(requireView(), R.string.api_key_invalid, Snackbar.LENGTH_SHORT).show();
// Save the original key
MainApplication.getSharedPreferences().edit().putString(
"pref_androidacy_api_token", originalApiKeyRef[0]).apply();
MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit().putString("pref_androidacy_api_token", originalApiKeyRef[0]).apply();
// Re-show the dialog with an error
prefAndroidacyRepoApiKey.performClick();
// Show error
prefAndroidacyRepoApiKey.setDialogMessage(getString(R.string.api_key_invalid));
});
} else {
// If the key is the same as the original, just show a snack bar
if (apiKey.equals(originalApiKeyRef[0])) {
new Handler(Looper.getMainLooper()).post(() -> Snackbar.make(requireView(), R.string.api_key_unchanged, Snackbar.LENGTH_SHORT).show());
return;
}
boolean valid = false;
try {
valid = AndroidacyRepoData.getInstance().isValidToken(apiKey);
} catch (IOException ignored) {}
} catch (IOException | NoSuchAlgorithmException ignored) {}
// If the key is valid, save it
if (valid) {
originalApiKeyRef[0] = apiKey;
RepoManager.getINSTANCE().getAndroidacyRepoData().setToken(apiKey);
MainApplication.getSharedPreferences().edit().putString(
"pref_androidacy_repo_api_key", apiKey).apply();
new Handler(Looper.getMainLooper()).post(() -> Snackbar.make(requireView(),
R.string.api_key_valid, Snackbar.LENGTH_SHORT).show());
MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit().putString("pref_androidacy_api_token", apiKey).apply();
// Snackbar with success and restart button
new Handler(Looper.getMainLooper()).post(() -> {
Snackbar.make(requireView(), R.string.api_key_valid, Snackbar.LENGTH_SHORT).show();
// Show dialog to restart app with ok button
new MaterialAlertDialogBuilder(this.requireContext())
.setTitle(R.string.restart)
.setMessage(R.string.api_key_restart)
.setNeutralButton(android.R.string.ok, (dialog, which) -> {
// User clicked OK button
Intent mStartActivity = new Intent(requireContext(), MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
int mPendingIntentId = 123456;
// If < 23, FLAG_IMMUTABLE is not available
PendingIntent mPendingIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId,
mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} else {
mPendingIntent = PendingIntent.getActivity(requireContext(), mPendingIntentId,
mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT);
}
AlarmManager mgr = (AlarmManager) requireContext().getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Restarting app to save token preference: " + newValue);
}
System.exit(0); // Exit app process
})
.show();
});
} else {
new Handler(Looper.getMainLooper()).post(() -> {
Snackbar.make(requireView(), R.string.api_key_invalid, Snackbar.LENGTH_SHORT).show();
// Save the original key
MainApplication.getSharedPreferences().edit().putString(
MainApplication.getINSTANCE().getSharedPreferences("androidacy", 0).edit().putString(
"pref_androidacy_api_token", originalApiKeyRef[0]).apply();
// Re-show the dialog with an error
prefAndroidacyRepoApiKey.performClick();
@ -575,6 +691,8 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
}).start();
return true;
});
// make sure the preference is visible if repo is enabled
prefAndroidacyRepoApiKey.setVisible(RepoManager.getINSTANCE().getAndroidacyRepoData().isEnabled());
}
@SuppressLint("RestrictedApi")
@ -589,7 +707,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
if (preference == null) continue;
final int index = i;
preference.setOnPreferenceClickListener(preference1 -> {
sharedPreferences.edit().putBoolean("pref_custom_repo_" + index + "_enabled", false).apply();
sharedPreferences.edit().remove("pref_custom_repo_" + index + "_enabled").apply();
customRepoManager.removeRepo(index);
updateCustomRepoList(false);
return true;
@ -615,13 +733,12 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
String text = String.valueOf(input.getText());
if (customRepoManager.canAddRepo(text)) {
final CustomRepoData customRepoData = customRepoManager.addRepo(text);
customRepoData.setEnabled(true);
new Thread("Add Custom Repo Thread") {
@Override
public void run() {
try {
customRepoData.quickPrePopulate();
} catch (IOException | JSONException e) {
} catch (IOException | JSONException | NoSuchAlgorithmException e) {
Log.e(TAG, "Failed to preload repo values", e);
}
UiThreadHandler.handler.post(() -> updateCustomRepoList(false));

@ -1,5 +1,6 @@
package com.fox2code.mmm.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
@ -18,6 +19,10 @@ import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.androidacy.AndroidacyUtil;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.repo.RepoManager;
import com.google.net.cronet.okhttptransport.CronetCallFactory;
import com.google.net.cronet.okhttptransport.CronetInterceptor;
import org.chromium.net.CronetEngine;
import java.io.ByteArrayOutputStream;
import java.io.File;
@ -48,7 +53,6 @@ import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.brotli.BrotliInterceptor;
import okhttp3.dnsoverhttps.DnsOverHttps;
import okio.BufferedSink;
@ -58,10 +62,7 @@ public class Http {
private static final OkHttpClient httpClientDoH;
private static final OkHttpClient httpClientWithCache;
private static final OkHttpClient httpClientWithCacheDoH;
private static final OkHttpClient httpClientNoRedirect;
private static final OkHttpClient httpClientNoRedirectDoH;
private static final FallBackDNS fallbackDNS;
private static final CDNCookieJar cookieJar;
private static final String androidacyUA;
private static final boolean hasWebView;
private static String needCaptchaAndroidacyHost;
@ -97,20 +98,10 @@ public class Http {
httpclientBuilder.connectTimeout(15, TimeUnit.SECONDS);
httpclientBuilder.writeTimeout(15, TimeUnit.SECONDS);
httpclientBuilder.readTimeout(15, TimeUnit.SECONDS);
httpclientBuilder.addInterceptor(BrotliInterceptor.INSTANCE);
httpclientBuilder.proxy(Proxy.NO_PROXY); // Do not use system proxy
Dns dns = Dns.SYSTEM;
try {
InetAddress[] cloudflareBootstrap = new InetAddress[]{
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")};
InetAddress[] cloudflareBootstrap = new InetAddress[]{InetAddress.getByName("162.159.36.1"), InetAddress.getByName("162.159.46.1"), InetAddress.getByName("1.1.1.1"), InetAddress.getByName("1.0.0.1"), InetAddress.getByName("162.159.132.53"), InetAddress.getByName("2606:4700:4700::1111"), InetAddress.getByName("2606:4700:4700::1001"), InetAddress.getByName("2606:4700:4700::0064"), InetAddress.getByName("2606:4700:4700::6400")};
dns = s -> {
if ("cloudflare-dns.com".equals(s)) {
return Arrays.asList(cloudflareBootstrap);
@ -119,21 +110,16 @@ public class Http {
};
httpclientBuilder.dns(dns);
httpclientBuilder.cookieJar(new CDNCookieJar());
dns = new DnsOverHttps.Builder().client(httpclientBuilder.build()).url(
Objects.requireNonNull(HttpUrl.parse("https://cloudflare-dns.com/dns-query")))
.bootstrapDnsHosts(cloudflareBootstrap).resolvePrivateAddresses(true).build();
dns = new DnsOverHttps.Builder().client(httpclientBuilder.build()).url(Objects.requireNonNull(HttpUrl.parse("https://cloudflare-dns.com/dns-query"))).bootstrapDnsHosts(cloudflareBootstrap).resolvePrivateAddresses(true).build();
} catch (UnknownHostException | RuntimeException e) {
Log.e(TAG, "Failed to init DoH", e);
}
httpclientBuilder.cookieJar(CookieJar.NO_COOKIES);
// User-Agent format was agreed on telegram
if (hasWebView) {
androidacyUA = WebSettings.getDefaultUserAgent(mainApplication)
.replace("wv", "") + "FoxMmm/" + BuildConfig.VERSION_CODE;
androidacyUA = WebSettings.getDefaultUserAgent(mainApplication).replace("wv", "") + " FoxMMM/" + BuildConfig.VERSION_CODE;
} else {
androidacyUA = "Mozilla/5.0 (Linux; Android " + Build.VERSION.RELEASE + "; " + Build.DEVICE + ")" +
" AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Mobile Safari/537.36" +
" FoxMmm/" + BuildConfig.VERSION_CODE;
androidacyUA = "Mozilla/5.0 (Linux; Android " + Build.VERSION.RELEASE + "; " + Build.DEVICE + ")" + " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Mobile Safari/537.36" + " FoxMmm/" + BuildConfig.VERSION_CODE;
}
httpclientBuilder.addInterceptor(chain -> {
Request.Builder request = chain.request().newBuilder();
@ -141,8 +127,7 @@ public class Http {
String host = chain.request().url().host();
if (host.endsWith(".androidacy.com")) {
request.header("User-Agent", androidacyUA);
} else if (!(host.equals("github.com") || host.endsWith(".github.com") ||
host.endsWith(".jsdelivr.net") || host.endsWith(".githubusercontent.com"))) {
} else if (!(host.equals("github.com") || host.endsWith(".github.com") || host.endsWith(".jsdelivr.net") || host.endsWith(".githubusercontent.com"))) {
if (InstallerInitializer.peekMagiskPath() != null) {
request.header("User-Agent", // Declare Magisk version to the server
"Magisk/" + InstallerInitializer.peekMagiskVersion());
@ -154,20 +139,48 @@ public class Http {
}
return chain.proceed(request.build());
});
// Add cronet interceptor
// install cronet
/*try {
// Detect if cronet is installed
CronetProviderInstaller.installProvider(mainApplication);
} catch (Exception e) {
Log.e(TAG, "Failed to install cronet", e);
}*/
// init cronet
try {
// Load the cronet library
System.loadLibrary("cronet.108.0.5359.95");
CronetEngine.Builder builder = new CronetEngine.Builder(mainApplication);
builder.enableBrotli(true);
builder.enableHttp2(true);
builder.enableQuic(true);
// Cache size is 10MB
// Make the directory if it does not exist
File cacheDir = new File(mainApplication.getCacheDir(), "cronet");
if (!cacheDir.exists()) {
if (!cacheDir.mkdirs()) {
throw new IOException("Failed to create cronet cache directory");
}
}
builder.setStoragePath(mainApplication.getCacheDir().getAbsolutePath() + "/cronet");
builder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 10 * 1024 * 1024);
CronetEngine engine =
builder.build();
httpclientBuilder.addInterceptor(CronetInterceptor.newBuilder(engine).build());
} catch (Exception e) {
Log.e(TAG, "Failed to init cronet", e);
// Gracefully fallback to okhttp
}
// Fallback DNS cache responses in case request fail but already succeeded once in the past
fallbackDNS = new FallBackDNS(mainApplication, dns, "github.com", "api.github.com",
"raw.githubusercontent.com", "camo.githubusercontent.com",
"user-images.githubusercontent.com", "cdn.jsdelivr.net",
"img.shields.io", "magisk-modules-repo.github.io",
"www.androidacy.com", "api.androidacy.com",
"production-api.androidacy.com");
httpclientBuilder.cookieJar(cookieJar = new CDNCookieJar(cookieManager));
fallbackDNS = new FallBackDNS(mainApplication, dns, "github.com", "api.github.com", "raw.githubusercontent.com", "camo.githubusercontent.com", "user-images.githubusercontent.com", "cdn.jsdelivr.net", "img.shields.io", "magisk-modules-repo.github.io", "www.androidacy.com", "api.androidacy.com", "production-api.androidacy.com");
httpclientBuilder.cookieJar(new CDNCookieJar(cookieManager));
httpclientBuilder.dns(Dns.SYSTEM);
httpClient = followRedirects(httpclientBuilder, true).build();
httpClientNoRedirect = followRedirects(httpclientBuilder, false).build();
followRedirects(httpclientBuilder, false).build();
httpclientBuilder.dns(fallbackDNS);
httpClientDoH = followRedirects(httpclientBuilder, true).build();
httpClientNoRedirectDoH = followRedirects(httpclientBuilder, false).build();
followRedirects(httpclientBuilder, false).build();
httpclientBuilder.cache(new Cache(new File(mainApplication.getCacheDir(), "http_cache"), 16L * 1024L * 1024L)); // 16Mib of cache
httpclientBuilder.dns(Dns.SYSTEM);
httpClientWithCache = followRedirects(httpclientBuilder, true).build();
@ -185,10 +198,6 @@ public class Http {
return doh ? httpClientDoH : httpClient;
}
public static OkHttpClient getHttpClientNoRedirect() {
return doh ? httpClientNoRedirectDoH : httpClientNoRedirect;
}
public static OkHttpClient getHttpClientWithCache() {
return doh ? httpClientWithCacheDoH : httpClientWithCache;
}
@ -212,7 +221,7 @@ public class Http {
public static boolean needCaptchaAndroidacy() {
return needCaptchaAndroidacyHost != null;
}
public static String needCaptchaAndroidacyHost() {
return needCaptchaAndroidacyHost;
}
@ -221,15 +230,19 @@ public class Http {
needCaptchaAndroidacyHost = null;
}
@SuppressLint("RestrictedApi")
@SuppressWarnings("resource")
public static byte[] doHttpGet(String url, boolean allowCache) throws IOException {
checkNeedBlockAndroidacyRequest(url);
Response response = (allowCache ? getHttpClientWithCache() : getHttpClient())
.newCall(new Request.Builder().url(url).get().build()).execute();
Response response =
(allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).get().build()).execute();
// 200/204 == success, 304 == cache valid
if (response.code() != 200 && response.code() != 204 &&
(response.code() != 304 || !allowCache)) {
if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) {
checkNeedCaptchaAndroidacy(url, response.code());
// If it's a 401, and an androidacy link, it's probably an invalid token
if (response.code() == 401 && AndroidacyUtil.isAndroidacyLink(url)) {
throw new HttpException("Androidacy token is invalid", 401);
}
throw new HttpException(response.code());
}
ResponseBody responseBody = response.body();
@ -242,23 +255,18 @@ public class Http {
}
public static byte[] doHttpPost(String url, String data, boolean allowCache) throws IOException {
return (byte[]) doHttpPostRaw(url, data, allowCache, false);
}
public static String doHttpPostRedirect(String url, String data, boolean allowCache) throws IOException {
return (String) doHttpPostRaw(url, data, allowCache, true);
return (byte[]) doHttpPostRaw(url, data, allowCache);
}
@SuppressWarnings("resource")
private static Object doHttpPostRaw(String url, String data, boolean allowCache, boolean isRedirect) throws IOException {
private static Object doHttpPostRaw(String url, String data, boolean allowCache) throws IOException {
checkNeedBlockAndroidacyRequest(url);
Response response = (isRedirect ? getHttpClientNoRedirect() : allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).post(JsonRequestBody.from(data)).header("Content-Type", "application/json").build()).execute();
if (isRedirect && response.isRedirect()) {
Response response = (allowCache ? getHttpClientWithCache() : getHttpClient()).newCall(new Request.Builder().url(url).post(JsonRequestBody.from(data)).header("Content-Type", "application/json").build()).execute();
if (response.isRedirect()) {
return response.request().url().uri().toString();
}
// 200/204 == success, 304 == cache valid
if (response.code() != 200 && response.code() != 204 &&
(response.code() != 304 || !allowCache)) {
if (response.code() != 200 && response.code() != 204 && (response.code() != 304 || !allowCache)) {
checkNeedCaptchaAndroidacy(url, response.code());
throw new HttpException(response.code());
}
@ -320,61 +328,15 @@ public class Http {
return androidacyUA;
}
public static String getMagiskUA() {
return "Magisk/" + InstallerInitializer.peekMagiskVersion();
}
public static void setDoh(boolean doh) {
Log.d(TAG, "DoH: " + Http.doh + " -> " + doh);
Http.doh = doh;
}
public static String getAndroidacyCookies(String url) {
if (!AndroidacyUtil.isAndroidacyLink(url)) return "";
return cookieJar.getAndroidacyCookies(url);
}
public static boolean hasWebView() {
return hasWebView;
}
/**
* Change URL to appropriate url and force Magisk link to use latest version.
*/
public static String updateLink(String string) {
if (string.startsWith("https://cdn.jsdelivr.net/gh/Magisk-Modules-Repo/")) {
String tmp = string.substring(48);
int start = tmp.lastIndexOf('@'), end = tmp.lastIndexOf('/');
if ((end - 8) <= start) return string; // Skip if not a commit id
return "https://raw.githubusercontent.com/" + tmp.substring(0, start) + "/master" + string.substring(end);
}
if (string.startsWith("https://github.com/Magisk-Modules-Repo/")) {
int i = string.lastIndexOf("/archive/");
if (i != -1 && string.indexOf('/', i + 9) == -1)
return string.substring(0, i + 9) + "master.zip";
}
return string;
}
/**
* Change GitHub user-content url to jsdelivr url
* (Unused but kept as a documentation)
*/
public static String cdnIfyLink(String string) {
if (string.startsWith("https://raw.githubusercontent.com/")) {
String[] tokens = string.substring(34).split("/", 4);
if (tokens.length != 4) return string;
return "https://cdn.jsdelivr.net/gh/" + tokens[0] + "/" + tokens[1] + "@" + tokens[2] + "/" + tokens[3];
}
if (string.startsWith("https://github.com/")) {
int i = string.lastIndexOf("/archive/");
if (i == -1 || string.indexOf('/', i + 9) != -1) return string; // Not an archive link
String[] tokens = string.substring(19).split("/", 4);
return "https://cdn.jsdelivr.net/gh/" + tokens[0] + "/" + tokens[1] + "@" + tokens[2] + "/" + tokens[3];
}
return string;
}
public interface ProgressListener {
void onUpdate(int downloaded, int total, boolean done);
}
@ -462,17 +424,6 @@ public class Http {
}
}
String getAndroidacyCookies(String url) {
if (this.cookieManager != null) {
return this.cookieManager.getCookie(url);
}
StringBuilder stringBuilder = new StringBuilder();
for (Cookie cookie : this.androidacyCookies) {
stringBuilder.append(cookie.toString()).append("; ");
}
stringBuilder.setLength(stringBuilder.length() - 2);
return stringBuilder.toString();
}
}
/**

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_height="wrap_content" >
<CheckBox
android:id="@+id/checkbox"
style="?android:attr/smallIcon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"/>
</FrameLayout>

@ -1,14 +1,8 @@
<resources>
<!-- If translating, please remove "theme_values"
<string-array name="theme_values" translatable="false">
<item>system</item>
<item>dark</item>
<item>light</item>
</string-array>-->
<string-array name="theme_values_names">
<item>Dle systému</item>
<item>Tmavá</item>
<item>AMOLED Black</item>
<item>Světlá</item>
</string-array>
</resources>

@ -3,6 +3,7 @@
<string-array name="theme_values_names">
<item>Systemvorgabe</item>
<item>Dunkel</item>
<item>AMOLED Black</item>
<item>Hell</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>Προεπιλογή συστήματως</item>
<item>Σκωτεινό</item>
<item>AMOLED Black</item>
<item>Ανοιχτό</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>Sistema</item>
<item>Oscuro</item>
<item>AMOLED Black</item>
<item>Claro</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>Süsteem</item>
<item>Tume</item>
<item>AMOLED Black</item>
<item>Hele</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>Système</item>
<item>Sombre</item>
<item>AMOLED Black</item>
<item>Clair</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>Sistem</item>
<item>Gelap</item>
<item>AMOLED Black</item>
<item>Terang</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>Sistema</item>
<item>Scuro</item>
<item>AMOLED Black</item>
<item>Chiaro</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>システムの設定を使用</item>
<item>ダーク</item>
<item>AMOLED Black</item>
<item>ライト</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item comment="theme_values_names">Zgodny z systemem</item>
<item comment="theme_values_names">Ciemny</item>
<item>AMOLED Black</item>
<item comment="theme_values_names">Jasny</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>Sistema</item>
<item>Escuro</item>
<item>AMOLED Black</item>
<item>Claro</item>
</string-array>
</resources>

@ -4,6 +4,7 @@
<string-array name="theme_values_names">
<item>Sistem</item>
<item>Întunecată</item>
<item>AMOLED Black</item>
<item>Luminoasă</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>Как в системе</item>
<item>Тёмная</item>
<item>AMOLED Black</item>
<item>Светлая</item>
</string-array>
</resources>

@ -1,14 +1,8 @@
<resources>
<!-- If translating, please remove "theme_values"
<string-array name="theme_values" translatable="false">
<item>system</item>
<item>dark</item>
<item>light</item>
</string-array>-->
<string-array name="theme_values_names">
<item>Podľa systému</item>
<item>Tmavá</item>
<item>AMOLED Black</item>
<item>Svetlá</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>Sistem</item>
<item>Koyu</item>
<item>AMOLED Black</item>
<item>Açık</item>
</string-array>
</resources>

@ -22,6 +22,19 @@
<item name="chipStyle">@style/Widget.Material3.Chip.Choice.Dark</item>
</style>
<!-- Black monet theme, which is just dark monet theme with black background -->
<style name="Theme.MagiskModuleManager.Monet.Black" parent="Theme.MagiskModuleManager.Monet.Dark">
<item name="colorBackgroundFloating">@color/black</item>
<item name="android:windowBackground">@color/black</item>
<item name="boxBackgroundColor">@color/black</item>
<item name="cardBackgroundColor">@color/black</item>
<item name="backgroundColor">@color/black</item>
<item name="colorSurface">@color/black</item>
<!-- Darker variants of the colors -->
<item name="colorPrimaryVariant">@color/system_accent2_900</item>
<item name="colorSecondaryVariant">@color/system_accent2_900</item>
</style>
<style name="Widget.Material3.Chip.Choice.Dark" parent="Widget.Material3.Chip.Assist">
<item name="chipBackgroundColor">@color/system_accent2_700</item>
<item name="chipStrokeWidth">0dp</item>

@ -3,6 +3,7 @@
<string-array name="theme_values_names">
<item>Hệ thống</item>
<item>Tối</item>
<item>AMOLED Black</item>
<item>Sáng</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>跟随系统</item>
<item>深色模式</item>
<item>AMOLED Black</item>
<item>明亮模式</item>
</string-array>
</resources>

@ -2,6 +2,7 @@
<string-array name="theme_values_names">
<item>跟隨系統</item>
<item>深色主題</item>
<item>AMOLED Black</item>
<item>淺色主題</item>
</string-array>
</resources>

@ -3,12 +3,15 @@
<string-array name="theme_values" translatable="false">
<item>system</item>
<item>dark</item>
<item>black</item>
<item>light</item>
</string-array>
<string-array name="theme_values_names">
<item>System</item>
<item>Dark</item>
<item>AMOLED Black</item>
<item>Light</item>
</string-array>
<string-array name="permission_notification_dont_ask_again">Don't prompt again</string-array>
</resources>

@ -181,4 +181,21 @@
<string name="androidacy_failed_to_validate_token">Could not validate token for Androidacy. Please try again later.</string>
<string name="androidacy_server_down">Unable to contact Androidacy server. Check your connection and try again.</string>
<string name="androidacy_need_captcha">Androidacy update blocked by Captcha</string>
<string name="api_key_restart">API key has been changed. Restart the app to apply changes.</string>
<string name="api_key_unchanged">The API key you input is the same as the one already in use.</string>
<string name="permission_notification_title">Allow notifications?</string>
<string name="permission_notification_message">We need the notifications permission to notify you on app and module updates. If you don\'t grant this permission, background update checks will not run.</string>
<string name="permission_notification_grant">Grant permission</string>
<string name="background_update_check_permission_required">Please enable notifications to enable this option.</string>
<string name="dont_ask_again">Don\'t ask again</string>
<string name="force_black_theme_desc">Forces AMOLED black backgrounds when using dark theme.
Please be aware this may have contrast problems with some color themes.</string>
<string name="force_black_theme_pref">Force black theme</string>
<string name="androidacy_repo_disabled">This repo is currently disabled</string>
<string name="androidacy_repo_disabled_message">This build is missing client keys for the
Androidacy Repo.
Please
download the GitHub release if you\'d like to benefit from features like module reviews, automatic security checks, and more.</string>
<string name="download_full_app">Download full version</string>
<string name="repo_update_failed">Some repos have failed to update</string>
</resources>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme_MagiskModuleManager_ForceBlack">#000000</style>
</resources>

@ -83,6 +83,19 @@
<item name="chipStyle">@style/Widget.Material.Chip.Choice.Dark</item>
</style>
<!-- Black theme, which is just a dark theme with a black background -->
<style name="Theme.MagiskModuleManager.Black" parent="Theme.MagiskModuleManager.Dark">
<item name="colorBackgroundFloating">@color/black</item>
<item name="android:windowBackground">@color/black</item>
<item name="boxBackgroundColor">@color/black</item>
<item name="cardBackgroundColor">@color/black</item>
<item name="backgroundColor">@color/black</item>
<item name="colorSurface">@color/black</item>
<!-- Darker variants of the colors -->
<item name="colorPrimaryVariant">@color/system_accent2_900</item>
<item name="colorSecondaryVariant">@color/system_accent2_900</item>
</style>
<style name="Widget.Material.Chip.Choice.Dark" parent="Widget.MaterialComponents.Chip.Action">
<item name="chipBackgroundColor">@color/dark_chipBackgroundColor</item>
<item name="chipStrokeWidth">0dp</item>

@ -1,305 +1,304 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
app:key="pref_magisk_alt_repo"
app:title="@string/loading">
<SwitchPreferenceCompat
app:key="pref_magisk_alt_repo_enabled"
app:icon="@drawable/ic_baseline_extension_24"
app:switchTextOn="@string/repo_enabled"
app:key="pref_magisk_alt_repo_enabled"
app:singleLineTitle="false"
app:switchTextOff="@string/repo_disabled"
app:singleLineTitle="false" />
app:switchTextOn="@string/repo_enabled" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_magisk_alt_repo_website"
app:icon="@drawable/ic_baseline_language_24"
app:title="@string/website"
app:singleLineTitle="false" />
app:key="pref_magisk_alt_repo_website"
app:singleLineTitle="false"
app:title="@string/website" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_magisk_alt_repo_support"
app:icon="@drawable/ic_baseline_support_24"
app:title="@string/support"
app:singleLineTitle="false" />
app:key="pref_magisk_alt_repo_support"
app:singleLineTitle="false"
app:title="@string/support" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_magisk_alt_repo_donate"
app:icon="@drawable/ic_baseline_monetization_on_24"
app:title="@string/donate"
app:singleLineTitle="false" />
app:key="pref_magisk_alt_repo_donate"
app:singleLineTitle="false"
app:title="@string/donate" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_magisk_alt_repo_submit"
app:icon="@drawable/ic_baseline_upload_file_24"
app:title="@string/submit_modules"
app:singleLineTitle="false" />
app:key="pref_magisk_alt_repo_submit"
app:singleLineTitle="false"
app:title="@string/submit_modules" />
</PreferenceCategory>
<PreferenceCategory
app:key="pref_androidacy_repo"
app:title="@string/loading">
<SwitchPreferenceCompat
app:key="pref_androidacy_repo_enabled"
app:icon="@drawable/ic_baseline_extension_24"
app:switchTextOn="@string/repo_enabled"
app:key="pref_androidacy_repo_enabled"
app:singleLineTitle="false"
app:switchTextOff="@string/repo_disabled"
app:singleLineTitle="false" />
<!-- Allow user to set custom API key for Androidacy repo -->
app:switchTextOn="@string/repo_enabled" />
<!-- Initially hidden edittextpreference for pref_androidacy_api_token -->
<EditTextPreference
app:key="pref_androidacy_repo_api_key"
app:icon="@drawable/ic_baseline_vpn_key_24"
app:title="@string/api_key"
app:summary="@string/api_key_summary"
app:singleLineTitle="true"
app:dialogTitle="@string/api_key"
app:dialogIcon="@drawable/ic_baseline_vpn_key_24"
app:key="pref_androidacy_api_token"
app:singleLineTitle="false"
app:dependency="pref_androidacy_repo_enabled"
android:inputType="text"
android:selectAllOnFocus="true"
android:imeOptions="actionDone" />
app:isPreferenceVisible="false"
app:title="@string/api_key" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:key="pref_androidacy_test_mode"
app:dependency="pref_androidacy_repo_enabled"
app:icon="@drawable/ic_baseline_bug_report_24"
app:title="@string/androidacy_test_mode_pref"
app:key="pref_androidacy_test_mode"
app:singleLineTitle="false"
app:summary="@string/androidacy_test_mode_desc"
app:dependency="pref_androidacy_repo_enabled"
app:singleLineTitle="false" />
app:title="@string/androidacy_test_mode_pref" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_androidacy_repo_website"
app:icon="@drawable/ic_baseline_language_24"
app:title="@string/website"
app:singleLineTitle="false" />
app:key="pref_androidacy_repo_website"
app:singleLineTitle="false"
app:title="@string/website" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_androidacy_repo_support"
app:icon="@drawable/ic_baseline_telegram_24"
app:title="@string/support"
app:singleLineTitle="false" />
app:key="pref_androidacy_repo_support"
app:singleLineTitle="false"
app:title="@string/support" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_androidacy_repo_donate"
app:icon="@drawable/ic_patreon"
app:title="@string/donate"
app:singleLineTitle="false" />
app:key="pref_androidacy_repo_donate"
app:singleLineTitle="false"
app:title="@string/donate" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_androidacy_repo_submit"
app:icon="@drawable/ic_baseline_upload_file_24"
app:title="@string/submit_modules"
app:singleLineTitle="false" />
app:key="pref_androidacy_repo_submit"
app:singleLineTitle="false"
app:title="@string/submit_modules" />
<!-- <Preference
app:key="pref_show_captcha_webview"
app:icon="@drawable/ic_baseline_vpn_key_24"
app:title="@string/show_captcha"
app:singleLineTitle="false" /> -->
<Preference
app:key="pref_androidacy_ads_disclaimer"
app:icon="@drawable/ic_baseline_info_24"
app:enabled="false"
app:summary="@string/androidacy_repo_info"
app:singleLineTitle="false" />
app:icon="@drawable/ic_baseline_info_24"
app:key="pref_androidacy_ads_disclaimer"
app:singleLineTitle="false"
app:summary="@string/androidacy_repo_info" />
</PreferenceCategory>
<PreferenceCategory
app:key="pref_dg_magisk_repo"
app:title="@string/loading">
<SwitchPreferenceCompat
app:key="pref_dg_magisk_repo_enabled"
app:icon="@drawable/ic_baseline_extension_24"
app:switchTextOn="@string/repo_enabled"
app:key="pref_dg_magisk_repo_enabled"
app:singleLineTitle="false"
app:switchTextOff="@string/repo_disabled"
app:singleLineTitle="false" />
app:switchTextOn="@string/repo_enabled" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_dg_magisk_repo_website"
app:icon="@drawable/ic_baseline_language_24"
app:title="@string/website"
app:singleLineTitle="false" />
app:key="pref_dg_magisk_repo_website"
app:singleLineTitle="false"
app:title="@string/website" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_dg_magisk_repo_support"
app:icon="@drawable/ic_baseline_support_24"
app:title="@string/support"
app:singleLineTitle="false" />
app:key="pref_dg_magisk_repo_support"
app:singleLineTitle="false"
app:title="@string/support" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_dg_magisk_repo_donate"
app:icon="@drawable/ic_baseline_monetization_on_24"
app:title="@string/donate"
app:singleLineTitle="false" />
app:key="pref_dg_magisk_repo_donate"
app:singleLineTitle="false"
app:title="@string/donate" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_dg_magisk_repo_submit"
app:icon="@drawable/ic_baseline_upload_file_24"
app:title="@string/submit_modules"
app:singleLineTitle="false" />
app:key="pref_dg_magisk_repo_submit"
app:singleLineTitle="false"
app:title="@string/submit_modules" />
</PreferenceCategory>
<PreferenceCategory
app:key="pref_custom_repo_0"
app:title="@string/loading">
<SwitchPreferenceCompat
app:key="pref_custom_repo_0_enabled"
app:icon="@drawable/ic_baseline_extension_24"
app:switchTextOn="@string/repo_enabled"
app:key="pref_custom_repo_0_enabled"
app:singleLineTitle="false"
app:switchTextOff="@string/repo_disabled"
app:singleLineTitle="false" />
app:switchTextOn="@string/repo_enabled" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_0_website"
app:icon="@drawable/ic_baseline_language_24"
app:title="@string/website"
app:singleLineTitle="false" />
app:key="pref_custom_repo_0_website"
app:singleLineTitle="false"
app:title="@string/website" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_0_support"
app:icon="@drawable/ic_baseline_support_24"
app:title="@string/support"
app:singleLineTitle="false" />
app:key="pref_custom_repo_0_support"
app:singleLineTitle="false"
app:title="@string/support" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_0_donate"
app:icon="@drawable/ic_baseline_monetization_on_24"
app:title="@string/donate"
app:singleLineTitle="false" />
app:key="pref_custom_repo_0_donate"
app:singleLineTitle="false"
app:title="@string/donate" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_0_submit"
app:icon="@drawable/ic_baseline_upload_file_24"
app:title="@string/submit_modules"
app:singleLineTitle="false" />
app:key="pref_custom_repo_0_submit"
app:singleLineTitle="false"
app:title="@string/submit_modules" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_0_delete"
app:icon="@drawable/ic_baseline_delete_forever_24"
app:title="@string/remove_repo"
app:singleLineTitle="false" />
app:key="pref_custom_repo_0_delete"
app:singleLineTitle="false"
app:title="@string/remove_repo" />
</PreferenceCategory>
<PreferenceCategory
app:key="pref_custom_repo_1"
app:title="@string/loading">
<SwitchPreferenceCompat
app:key="pref_custom_repo_1_enabled"
app:icon="@drawable/ic_baseline_extension_24"
app:switchTextOn="@string/repo_enabled"
app:key="pref_custom_repo_1_enabled"
app:singleLineTitle="false"
app:switchTextOff="@string/repo_disabled"
app:singleLineTitle="false" />
app:switchTextOn="@string/repo_enabled" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_1_website"
app:icon="@drawable/ic_baseline_language_24"
app:title="@string/website"
app:singleLineTitle="false" />
app:key="pref_custom_repo_1_website"
app:singleLineTitle="false"
app:title="@string/website" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_1_support"
app:icon="@drawable/ic_baseline_support_24"
app:title="@string/support"
app:singleLineTitle="false" />
app:key="pref_custom_repo_1_support"
app:singleLineTitle="false"
app:title="@string/support" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_1_donate"
app:icon="@drawable/ic_baseline_monetization_on_24"
app:title="@string/donate"
app:singleLineTitle="false" />
app:key="pref_custom_repo_1_donate"
app:singleLineTitle="false"
app:title="@string/donate" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_1_submit"
app:icon="@drawable/ic_baseline_upload_file_24"
app:title="@string/submit_modules"
app:singleLineTitle="false" />
app:key="pref_custom_repo_1_submit"
app:singleLineTitle="false"
app:title="@string/submit_modules" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_1_delete"
app:icon="@drawable/ic_baseline_delete_forever_24"
app:title="@string/remove_repo"
app:singleLineTitle="false" />
app:key="pref_custom_repo_1_delete"
app:singleLineTitle="false"
app:title="@string/remove_repo" />
</PreferenceCategory>
<PreferenceCategory
app:key="pref_custom_repo_2"
app:title="@string/loading">
<SwitchPreferenceCompat
app:key="pref_custom_repo_2_enabled"
app:icon="@drawable/ic_baseline_extension_24"
app:switchTextOn="@string/repo_enabled"
app:key="pref_custom_repo_2_enabled"
app:singleLineTitle="false"
app:switchTextOff="@string/repo_disabled"
app:singleLineTitle="false" />
app:switchTextOn="@string/repo_enabled" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_2_website"
app:icon="@drawable/ic_baseline_language_24"
app:title="@string/website"
app:singleLineTitle="false" />
app:key="pref_custom_repo_2_website"
app:singleLineTitle="false"
app:title="@string/website" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_2_support"
app:icon="@drawable/ic_baseline_support_24"
app:title="@string/support"
app:singleLineTitle="false" />
app:key="pref_custom_repo_2_support"
app:singleLineTitle="false"
app:title="@string/support" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_2_donate"
app:icon="@drawable/ic_baseline_monetization_on_24"
app:title="@string/donate"
app:singleLineTitle="false" />
app:key="pref_custom_repo_2_donate"
app:singleLineTitle="false"
app:title="@string/donate" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_2_submit"
app:icon="@drawable/ic_baseline_upload_file_24"
app:title="@string/submit_modules"
app:singleLineTitle="false" />
app:key="pref_custom_repo_2_submit"
app:singleLineTitle="false"
app:title="@string/submit_modules" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_2_delete"
app:icon="@drawable/ic_baseline_delete_forever_24"
app:title="@string/remove_repo"
app:singleLineTitle="false" />
app:key="pref_custom_repo_2_delete"
app:singleLineTitle="false"
app:title="@string/remove_repo" />
</PreferenceCategory>
<PreferenceCategory
app:key="pref_custom_repo_3"
app:title="@string/loading">
<SwitchPreferenceCompat
app:key="pref_custom_repo_3_enabled"
app:icon="@drawable/ic_baseline_extension_24"
app:switchTextOn="@string/repo_enabled"
app:key="pref_custom_repo_3_enabled"
app:singleLineTitle="false"
app:switchTextOff="@string/repo_disabled"
app:singleLineTitle="false" />
app:switchTextOn="@string/repo_enabled" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_3_website"
app:icon="@drawable/ic_baseline_language_24"
app:title="@string/website"
app:singleLineTitle="false" />
app:key="pref_custom_repo_3_website"
app:singleLineTitle="false"
app:title="@string/website" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_3_support"
app:icon="@drawable/ic_baseline_support_24"
app:title="@string/support"
app:singleLineTitle="false" />
app:key="pref_custom_repo_3_support"
app:singleLineTitle="false"
app:title="@string/support" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_3_donate"
app:icon="@drawable/ic_baseline_monetization_on_24"
app:title="@string/donate"
app:singleLineTitle="false" />
app:key="pref_custom_repo_3_donate"
app:singleLineTitle="false"
app:title="@string/donate" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_3_submit"
app:icon="@drawable/ic_baseline_upload_file_24"
app:title="@string/submit_modules"
app:singleLineTitle="false" />
app:key="pref_custom_repo_3_submit"
app:singleLineTitle="false"
app:title="@string/submit_modules" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_3_delete"
app:icon="@drawable/ic_baseline_delete_forever_24"
app:title="@string/remove_repo"
app:singleLineTitle="false" />
app:key="pref_custom_repo_3_delete"
app:singleLineTitle="false"
app:title="@string/remove_repo" />
</PreferenceCategory>
<PreferenceCategory
app:key="pref_custom_repo_4"
app:title="@string/loading">
<SwitchPreferenceCompat
app:key="pref_custom_repo_4_enabled"
app:icon="@drawable/ic_baseline_extension_24"
app:switchTextOn="@string/repo_enabled"
app:key="pref_custom_repo_4_enabled"
app:singleLineTitle="false"
app:switchTextOff="@string/repo_disabled"
app:singleLineTitle="false" />
app:switchTextOn="@string/repo_enabled" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_4_website"
app:icon="@drawable/ic_baseline_language_24"
app:title="@string/website"
app:singleLineTitle="false" />
app:key="pref_custom_repo_4_website"
app:singleLineTitle="false"
app:title="@string/website" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_4_support"
app:icon="@drawable/ic_baseline_support_24"
app:title="@string/support"
app:singleLineTitle="false" />
app:key="pref_custom_repo_4_support"
app:singleLineTitle="false"
app:title="@string/support" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_4_donate"
app:icon="@drawable/ic_baseline_monetization_on_24"
app:title="@string/donate"
app:singleLineTitle="false" />
app:key="pref_custom_repo_4_donate"
app:singleLineTitle="false"
app:title="@string/donate" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_4_submit"
app:icon="@drawable/ic_baseline_upload_file_24"
app:title="@string/submit_modules"
app:singleLineTitle="false" />
app:key="pref_custom_repo_4_submit"
app:singleLineTitle="false"
app:title="@string/submit_modules" />
<com.fox2code.mmm.settings.LongClickablePreference
app:key="pref_custom_repo_4_delete"
app:icon="@drawable/ic_baseline_delete_forever_24"
app:title="@string/remove_repo"
app:singleLineTitle="false" />
app:key="pref_custom_repo_4_delete"
app:singleLineTitle="false"
app:title="@string/remove_repo" />
</PreferenceCategory>
<PreferenceCategory
app:key="pref_custom_add_repo"
app:title="@string/add_repo">
<Preference
app:key="pref_custom_add_repo_button"
app:icon="@drawable/ic_baseline_add_box_24"
app:title="@string/add_repo"
app:singleLineTitle="false" />
app:key="pref_custom_add_repo_button"
app:singleLineTitle="false"
app:title="@string/add_repo" />
</PreferenceCategory>
</PreferenceScreen>

@ -3,13 +3,15 @@
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="QueryAllPackagesPermission">
<application android:icon="@mipmap/ic_launcher">
<application android:icon="@mipmap/ic_launcher" android:enableOnBackInvokedCallback="true"
tools:targetApi="tiramisu">
<meta-data
android:name="io.sentry.auto-init"
android:value="false" />
<meta-data
android:name="io.sentry.dsn"
android:value="https://cdcdb0efca4a42a28df90e4b7f087347@sentry.androidacy.com/2" />
android:value="https://198c68516cb0412b9832204631a3fac8@o993586.ingest.sentry.io/4504069942804480" />
<!-- Sane value, but feel free to lower it -->
<meta-data
android:name="io.sentry.traces.sample-rate"
@ -30,5 +32,8 @@
<meta-data
android:name="io.sentry.sendDefaultPii"
android:value="false" />
<meta-data
android:name="io.sentry.traces.profiling.sample-rate"
android:value="0.5" />
</application>
</manifest>

@ -29,6 +29,10 @@ public class SentryMain {
public static final boolean IS_SENTRY_INSTALLED = true;
private static final String TAG = "SentryMain";
/**
* Initialize Sentry
* Sentry is used for crash reporting and performance monitoring. The SDK is explcitly configured not to send PII, and server side scrubbing of sensitive data is enabled (which also removes IP addresses)
*/
public static void initialize(final MainApplication mainApplication) {
SentryAndroid.init(mainApplication, options -> {
// If crash reporting is disabled, stop here.
@ -39,11 +43,9 @@ public class SentryMain {
// Sentry sends ABSOLUTELY NO Personally Identifiable Information (PII) by default.
// Already set to false by default, just set it again to make peoples feel safer.
options.setSendDefaultPii(false);
// It just tell if sentry should ping the sentry dsn to tell the app is running.
// This is not needed at all for crash reporting purposes, so disable it.
options.setEnableAutoSessionTracking(false);
// It just tell if sentry should ping the sentry dsn to tell the app is running. Useful for performance and profiling.
options.setEnableAutoSessionTracking(true);
// A screenshot of the app itself is only sent if the app crashes, and it only shows the last activity
// In addition, sentry is configured with a trusted third party other than sentry.io, and only trusted people have access to the sentry instance
// Add a callback that will be used before the event is sent to Sentry.
// With this callback, you can modify the event or, when returning null, also discard the event.
options.setBeforeSend((event, hint) -> {

@ -4,10 +4,16 @@ buildscript {
google()
mavenCentral()
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
project.ext.latestAboutLibsRelease = "10.5.0"
project.ext.sentryConfigFile = new File(rootDir, "sentry.properties").getAbsoluteFile()
project.ext.hasSentryConfig = sentryConfigFile.exists()
project.ext.sentryCli = [
logLevel: "debug",
flavorAware: true
]
project.ext.kotlin_version = "1.7.20"
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${latestAboutLibsRelease}"

@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxPermSize=512m -XX:ReservedCodeCacheSize=512m
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:ReservedCodeCacheSize=512m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects

@ -9,4 +9,4 @@ dependencyResolutionManagement {
}
}
rootProject.name = "MagiskModuleManager"
include ':app'
include ':app'
Loading…
Cancel
Save