Merge remote-tracking branch 'up/master'

pull/208/head
androidacy-user 2 years ago
commit d9ba93f41c

@ -1 +1,5 @@
blank_issues_enabled: false
blank_issues_enabled: false
contact_links:
- name: Fox2Code Telegram group
url: https://t.me/Fox2Code_Chat
about: Join the Telegram group if you want to discuss with the community.

@ -0,0 +1,8 @@
---
name: Question
about: Question about this project
title: ''
labels: question
assignees: ''
---

1
.gitignore vendored

@ -1,5 +1,6 @@
*.iml
.gradle
/sentry.properties
/local.properties
/.idea/
.DS_Store

@ -11,6 +11,8 @@ Index:
- [Properties](DEVELOPERS.md#properties)
- [ANSI Styling](DEVELOPERS.md#ansi-styling)
- [Installer commands](DEVELOPERS.md#installer-commands)
- [Developer mode](DEVELOPERS.md#developer-mode)
- [End note](DEVELOPERS.md#end-note)
## Special notes
@ -79,6 +81,7 @@ support=<url>
donate=<url>
config=<package>
changeBoot=<boolean>
mmtReborn=<boolean>
```
Note: All urls must start with `https://`, or else will be ignored
Note²: For `minMagisk`, `XX.Y` is parsed as `XXY00`, so you can just put the Magisk version name.
@ -93,6 +96,8 @@ Note²: For `minMagisk`, `XX.Y` is parsed as `XXY00`, so you can just put the Ma
- `config` package name of the application that configure your module
(Note: The icon won't appear in the module list if the module or target app is not installed)
- `changeBoot` tell the manager the module may change the boot image
- `mmtReborn` tell the manager to use [MMT-Reborn](https://github.com/iamlooper/MMT-Reborn) logging style
(For example `- Hello world` will be transformed to `[*] Hello world`, do not apply to modules installed from storage)
Note: Fox's Mmm use fallback
[here](app/src/main/java/com/fox2code/mmm/utils/PropUtils.java#L36)
@ -170,6 +175,22 @@ mmm_exec setSupportLink https://github.com/Fox2Code/FoxMagiskModuleManager
[You may look at the examples modules and their codes.](examples)
## Developer mode
FoxMMM contain a developer mode to unlock unstable, dangerous, or experimental features.
To unlock developer mode you must do the following in this order:
- Go to setting
- Open theme selector option
- Close it without selecting a theme
- Click on show license
- Go back to setting page
- Click on source code button
To disable dev mode just repeat the exact same steps.
## End Note
Have fun with the API making your user install experience a unique experience
Also there is the source of the app icon

@ -53,15 +53,15 @@ settings() {
fi
}
if [ $MAGISK_VER_CODE -ge 20400 ]; 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
fi
#################
# Legacy Support
#################
#######################################################
# Legacy Support + compat mode for MMT Reborn template
#######################################################
TMPDIR=/dev/tmp
PERSISTDIR=/sbin/.magisk/mirror/persist
@ -98,6 +98,7 @@ abort() {
rm -rf $TMPDIR 2>/dev/null
mkdir -p $TMPDIR
chcon u:object_r:system_file:s0 $TMPDIR || true
cd $TMPDIR
# Preperation for flashable zips
@ -121,7 +122,9 @@ $BOOTMODE && boot_actions || recovery_actions
unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2
[ ! -f $TMPDIR/module.prop ] && abort "! Unable to extract zip file!"
$BOOTMODE && MODDIRNAME=modules_update || MODDIRNAME=modules
[ -z "$NVBASE" ] && NVBASE="/data/adb"
MODDIRNAME=modules
$BOOTMODE && MODDIRNAME=modules_update
MODULEROOT=$NVBASE/$MODDIRNAME
MODID=`grep_prop id $TMPDIR/module.prop`
MODNAME=`grep_prop name $TMPDIR/module.prop`
@ -166,6 +169,50 @@ if is_legacy_script; then
ui_print "- Setting permissions"
set_permissions
elif [ -n "$MMM_MMT_REBORN" ]; then
# https://github.com/iamlooper/MMT-Reborn
ui_print "[*] Using FoxMMM MMT-Reborn compatibility mode"
load_vksel() { source "$MODPATH/addon/Volume-Key-Selector/install.sh"; }
rmtouch() { [[ -e "$1" ]] && rm -rf "$1" 2>/dev/null; }
unzip -o "$ZIPFILE" -d "$MODPATH" >&2
# Load install script
source "$MODPATH/setup.sh"
# Remove all old files before doing installation if want to
"$CLEANSERVICE" && rm -rf "/data/adb/modules/$MODID"
# Enable debugging if true
"$DEBUG" && set -x || set +x
# Print mod info
info_print
# Auto vskel load
"$AUTOVKSEL" && load_vksel
# Main
init_main
# Skip mount
"$SKIPMOUNT" && touch "$MODPATH/skip_mount"
# Set permissions
set_permissions
# Remove stuffs that don't belong to modules
rmtouch "$MODPATH/META-INF"
rmtouch "$MODPATH/addon"
rmtouch "$MODPATH/setup.sh"
rmtouch "$MODPATH/LICENSE"
rmtouch "$MODPATH/README.md"
rmtouch "$MODPATH/system/bin/placeholder"
rmtouch "$MODPATH/zygisk/placeholder"
ui_print "[*] Exiting FoxMMM MMT-Reborn compatibility mode"
sleep 0.5
else
print_modname
@ -198,13 +245,20 @@ if $BOOTMODE; then
fi
# Copy over custom sepolicy rules
if [ -f $MODPATH/sepolicy.rule -a -e $PERSISTDIR ]; then
ui_print "- Installing custom sepolicy patch"
# Remove old recovery logs (which may be filling partition) to make room
rm -f $PERSISTDIR/cache/recovery/*
PERSISTMOD=$PERSISTDIR/magisk/$MODID
mkdir -p $PERSISTMOD
cp -af $MODPATH/sepolicy.rule $PERSISTMOD/sepolicy.rule || abort "! Insufficient partition size"
if ! type copy_sepolicy_rules &>/dev/null; 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"
fi
else
if [ -f $MODPATH/sepolicy.rule ]; then
ui_print "- Installing custom sepolicy rules"
copy_sepolicy_rules
fi
fi
# Remove stuff that doesn't belong to modules and clean up any empty directories

@ -20,14 +20,15 @@ import java.util.HashMap;
// See https://docs.github.com/en/rest/reference/repos#releases
public class AppUpdateManager {
public static final int FLAG_COMPAT_LOW_QUALITY = 0x01;
public static final int FLAG_COMPAT_NO_EXT = 0x02;
public static final int FLAG_COMPAT_MAGISK_CMD = 0x04;
public static final int FLAG_COMPAT_NEED_32BIT = 0x08;
public static final int FLAG_COMPAT_MALWARE = 0x10;
public static final int FLAG_COMPAT_NO_ANSI = 0x20;
public static final int FLAG_COMPAT_FORCE_ANSI = 0x40;
public static final int FLAG_COMPAT_FORCE_HIDE = 0x80;
public static final int FLAG_COMPAT_LOW_QUALITY = 0x0001;
public static final int FLAG_COMPAT_NO_EXT = 0x0002;
public static final int FLAG_COMPAT_MAGISK_CMD = 0x0004;
public static final int FLAG_COMPAT_NEED_32BIT = 0x0008;
public static final int FLAG_COMPAT_MALWARE = 0x0010;
public static final int FLAG_COMPAT_NO_ANSI = 0x0020;
public static final int FLAG_COMPAT_FORCE_ANSI = 0x0040;
public static final int FLAG_COMPAT_FORCE_HIDE = 0x0080;
public static final int FLAG_COMPAT_MMT_REBORN = 0x0100;
private static final String TAG = "AppUpdateManager";
private static final AppUpdateManager INSTANCE = new AppUpdateManager();
private static final String RELEASES_API_URL =
@ -213,6 +214,9 @@ public class AppUpdateManager {
case "forceHide":
value |= FLAG_COMPAT_FORCE_HIDE;
break;
case "mmtReborn":
value |= FLAG_COMPAT_MMT_REBORN;
break;
}
}
compatDataId.put(line.substring(0, i), value);

@ -14,6 +14,7 @@ public class Constants {
public static final String EXTRA_INSTALL_NAME = "extra_install_name";
public static final String EXTRA_INSTALL_CONFIG = "extra_install_config";
public static final String EXTRA_INSTALL_CHECKSUM = "extra_install_checksum";
public static final String EXTRA_INSTALL_MMT_REBORN = "extra_install_mmt_reborn";
public static final String EXTRA_INSTALL_NO_EXTENSIONS = "extra_install_no_extensions";
public static final String EXTRA_INSTALL_TEST_ROOTLESS = "extra_install_test_rootless";
public static final String EXTRA_ANDROIDACY_COMPAT_LEVEL = "extra_androidacy_compat_level";

@ -31,19 +31,21 @@ import com.fox2code.mmm.module.ModuleViewAdapter;
import com.fox2code.mmm.module.ModuleViewListBuilder;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.settings.SettingsActivity;
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.progressindicator.LinearProgressIndicator;
import eightbitlab.com.blurview.BlurView;
import eightbitlab.com.blurview.RenderScriptBlur;
import io.sentry.android.core.SentryAndroid;
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;
public final ModuleViewListBuilder moduleViewListBuilder;
public LinearProgressIndicator progressIndicator;
private ModuleViewAdapter moduleViewAdapter;
@ -59,6 +61,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
private RecyclerView moduleList;
private CardView searchCard;
private SearchView searchView;
private NoodleDebug noodleDebug;
private boolean initMode;
public MainActivity() {
@ -75,6 +78,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
@Override
protected void onCreate(Bundle savedInstanceState) {
this.initMode = true;
noodleDebugState = MainApplication.isDeveloper();
BackgroundUpdateChecker.onMainActivityCreate(this);
super.onCreate(savedInstanceState);
this.setActionBarExtraMenuButton(R.drawable.ic_baseline_settings_24, v -> {
@ -106,6 +110,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
this.moduleList = findViewById(R.id.module_list);
this.searchCard = findViewById(R.id.search_card);
this.searchView = findViewById(R.id.search_bar);
this.noodleDebug = new NoodleDebug(this, R.id.noodle_debug);
this.moduleViewAdapter = new ModuleViewAdapter();
this.moduleList.setAdapter(this.moduleViewAdapter);
this.moduleList.setLayoutManager(new LinearLayoutManager(this));
@ -152,25 +157,33 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED);
if (!MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
noodleDebug.setEnabled(noodleDebugState);
noodleDebug.bind();
ModuleManager.getINSTANCE().scan();
moduleViewListBuilder.appendInstalledModules();
ModuleManager.getINSTANCE().runAfterScan(
moduleViewListBuilder::appendInstalledModules);
this.commonNext();
}
@Override
public void onFailure(int error) {
Log.i(TAG, "Failed to get magisk path!");
moduleViewListBuilder.addNotification(NotificationType.NO_ROOT);
noodleDebug.setEnabled(noodleDebugState);
noodleDebug.bind();
moduleViewListBuilder.addNotification(
InstallerInitializer.getErrorNotification());
this.commonNext();
}
public void commonNext() {
NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug();
swipeRefreshBlocker = System.currentTimeMillis() + 5_000L;
updateScreenInsets(); // Fix an edge case
if (MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE);
if (!Http.hasWebView()) // Check Http for WebView availability
moduleViewListBuilder.addNotification(NotificationType.NO_WEB_VIEW);
noodleDebug.push("Apply");
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
runOnUiThread(() -> {
progressIndicator.setIndeterminate(false);
@ -179,11 +192,14 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
updateScreenInsets(getResources().getConfiguration());
});
Log.i(TAG, "Scanning for modules!");
noodleDebug.replace("Initialize Update");
final int max = ModuleManager.getINSTANCE().getUpdatableModuleCount();
if (RepoManager.getINSTANCE().getCustomRepoManager().needUpdate()) {
Log.w(TAG, "Need update on create?");
}
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) :() ->
@ -194,13 +210,17 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
} else {
// Compatibility data still needs to be updated
AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager();
noodleDebug.replace("Check App Update");
if (BuildConfig.ENABLE_AUTO_UPDATER && appUpdateManager.checkUpdate(true))
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
noodleDebug.replace("Check Json Update");
if (max != 0) {
int current = 0;
noodleDebug.push("");
for (LocalModuleInfo localModuleInfo :
ModuleManager.getINSTANCE().getModules().values()) {
if (localModuleInfo.updateJson != null) {
noodleDebug.replace(localModuleInfo.id);
try {
localModuleInfo.checkModuleUpdate();
} catch (Exception e) {
@ -214,6 +234,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
+ (PRECISION * 0.75F)), true));
}
}
noodleDebug.pop();
}
}
runOnUiThread(() -> {
@ -223,11 +244,16 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
setActionBarBackground(null);
updateScreenInsets(getResources().getConfiguration());
});
moduleViewListBuilder.appendRemoteModules();
noodleDebug.replace("Apply");
RepoManager.getINSTANCE().runAfterUpdate(
moduleViewListBuilder::appendRemoteModules);
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
noodleDebug.pop();
Log.i(TAG, "Finished app opening state!");
noodleDebug.unbind();
}
}, true);
ExternalHelper.INSTANCE.refreshHelper(this);
this.initMode = false;
}
@ -308,6 +334,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
this.updateBlurState();
this.moduleViewListBuilder.setQuery(null);
Log.i(TAG, "Item After");
noodleDebugState = MainApplication.isDeveloper();
this.moduleViewListBuilder.refreshNotificationsUI(this.moduleViewAdapter);
InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
@Override
@ -317,19 +344,26 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED);
if (!MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
noodleDebug.setEnabled(noodleDebugState);
noodleDebug.bind();
ModuleManager.getINSTANCE().scan();
moduleViewListBuilder.appendInstalledModules();
ModuleManager.getINSTANCE().runAfterScan(
moduleViewListBuilder::appendInstalledModules);
this.commonNext();
}
@Override
public void onFailure(int error) {
moduleViewListBuilder.addNotification(NotificationType.NO_ROOT);
moduleViewListBuilder.addNotification(
InstallerInitializer.getErrorNotification());
noodleDebug.setEnabled(noodleDebugState);
noodleDebug.bind();
this.commonNext();
}
public void commonNext() {
Log.i(TAG, "Common Before");
NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug();
if (MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE);
if (!NotificationType.NO_INTERNET.shouldRemove())
@ -337,11 +371,13 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
else if (AppUpdateManager.getAppUpdateManager().checkUpdate(false))
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
RepoManager.getINSTANCE().updateEnabledStates();
noodleDebug.push("");
if (RepoManager.getINSTANCE().getCustomRepoManager().needUpdate()) {
runOnUiThread(() -> {
progressIndicator.setIndeterminate(false);
progressIndicator.setMax(PRECISION);
});
noodleDebug.replace("Check Update");
RepoManager.getINSTANCE().update(value -> runOnUiThread(() ->
progressIndicator.setProgressCompat(
(int) (value * PRECISION), true)));
@ -350,10 +386,14 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
progressIndicator.setVisibility(View.GONE);
});
}
moduleViewListBuilder.appendRemoteModules();
noodleDebug.replace("Apply");
RepoManager.getINSTANCE().runAfterUpdate(
moduleViewListBuilder::appendRemoteModules);
Log.i(TAG, "Common Before applyTo");
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
noodleDebug.pop();
Log.i(TAG, "Common After");
noodleDebug.unbind();
}
});
this.initMode = false;
@ -377,14 +417,49 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
this.swipeRefreshBlocker = System.currentTimeMillis() + 5_000L;
// this.swipeRefreshLayout.setRefreshing(true); ??
new Thread(() -> {
noodleDebug.setEnabled(noodleDebugState);
NoodleDebug noodleDebug = this.noodleDebug.bind();
Http.cleanDnsCache(); // Allow DNS reload from network
RepoManager.getINSTANCE().update(value -> runOnUiThread(() ->
this.progressIndicator.setProgressCompat(
(int) (value * PRECISION), true)));
if (!NotificationType.NO_INTERNET.shouldRemove())
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)));
if (!NotificationType.NO_INTERNET.shouldRemove()) {
moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
else if (AppUpdateManager.getAppUpdateManager().checkUpdate(true))
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
} else {
// Compatibility data still needs to be updated
AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager();
noodleDebug.replace("Check App Update");
if (BuildConfig.ENABLE_AUTO_UPDATER && appUpdateManager.checkUpdate(true))
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
noodleDebug.replace("Check Json Update");
if (max != 0) {
int current = 0;
noodleDebug.push("");
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);
}
current++;
final int currentTmp = current;
runOnUiThread(() -> progressIndicator.setProgressCompat(
(int) ((1F * currentTmp / max) * PRECISION * 0.25F
+ (PRECISION * 0.75F)), true));
}
}
noodleDebug.pop();
}
}
noodleDebug.replace("Apply");
runOnUiThread(() -> {
this.progressIndicator.setVisibility(View.GONE);
this.swipeRefreshLayout.setRefreshing(false);
@ -393,8 +468,11 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
this.moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
}
RepoManager.getINSTANCE().updateEnabledStates();
this.moduleViewListBuilder.appendRemoteModules();
RepoManager.getINSTANCE().runAfterUpdate(
moduleViewListBuilder::appendRemoteModules);
this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
noodleDebug.pop();
noodleDebug.unbind();
},"Repo update thread").start();
}

@ -21,14 +21,13 @@ import androidx.emoji2.text.FontRequestEmojiCompatConfig;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.FoxApplication;
import com.fox2code.foxcompat.FoxThemeWrapper;
import com.fox2code.foxcompat.internal.FoxProcessExt;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.utils.GMSProviderInstaller;
import com.fox2code.mmm.utils.Http;
import com.fox2code.rosettax.LanguageSwitcher;
import com.topjohnwu.superuser.Shell;
import java.io.IOException;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
@ -44,9 +43,6 @@ import io.noties.markwon.syntax.Prism4jThemeDefault;
import io.noties.markwon.syntax.SyntaxHighlightPlugin;
import io.noties.prism4j.Prism4j;
import io.noties.prism4j.annotations.PrismBundle;
import io.sentry.JsonObjectWriter;
import io.sentry.NoOpLogger;
import io.sentry.android.core.SentryAndroid;
@PrismBundle(
includeAll = true,
@ -62,7 +58,10 @@ public class MainApplication extends FoxApplication
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 SharedPreferences bootSharedPreferences;
private static String relPackageName = BuildConfig.APPLICATION_ID;
private static MainApplication INSTANCE;
private static boolean firstBoot;
@ -88,13 +87,20 @@ public class MainApplication extends FoxApplication
ComponentName componentName = intent.getComponent();
String packageName = componentName != null ?
componentName.getPackageName() : intent.getPackage();
if (!BuildConfig.APPLICATION_ID.equalsIgnoreCase(packageName)) {
if (!BuildConfig.APPLICATION_ID.equalsIgnoreCase(packageName) &&
!relPackageName.equals(packageName)) {
// Code safeguard, we should never reach here.
throw new IllegalArgumentException("Can't add secret to outbound Intent");
}
intent.putExtra("secret", secret);
}
// Is application wrapped, and therefore must reduce it's feature set.
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isWrapped() {
return wrapped;
}
public static boolean checkSecret(Intent intent) {
return intent != null && intent.getLongExtra("secret", ~secret) == secret;
}
@ -154,7 +160,7 @@ public class MainApplication extends FoxApplication
}
public static boolean isBackgroundUpdateCheckEnabled() {
return getSharedPreferences().getBoolean("pref_background_update_check", true);
return !wrapped && getSharedPreferences().getBoolean("pref_background_update_check", true);
}
public static boolean isAndroidacyTestMode() {
@ -174,10 +180,10 @@ public class MainApplication extends FoxApplication
getSharedPreferences().edit().putBoolean("has_root_access", bool).apply();
}
public static boolean isCrashReportingEnabled() {
return getSharedPreferences().getBoolean(
"crash_reporting", BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING);
}
/*public static boolean isCrashReportingEnabled() {
return getSharedPreferences().getBoolean("pref_crash_reporting",
BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING && !BuildConfig.DEBUG);
}*/
public static SharedPreferences getBootSharedPreferences() {
return bootSharedPreferences;
@ -310,6 +316,7 @@ public class MainApplication extends FoxApplication
@Override
public void onCreate() {
if (INSTANCE == null) INSTANCE = this;
relPackageName = this.getPackageName();
super.onCreate();
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
DynamicColors.applyToActivitiesIfAvailable(this,
@ -352,7 +359,9 @@ public class MainApplication extends FoxApplication
Log.d("MainApplication", "Emoji compat loaded!");
}, "Emoji compat init.").start();
}
SentryAndroid.init(this, options -> {
/*SentryAndroid.init(this, options -> {
options.addIntegration(new FragmentLifecycleIntegration(this, true, false));
// Note: Sentry library only take a screenshot of Fox Magisk Module Manager.
// The screen shot doesn't and cannot contain other applications (if in multi windows)
// status bar and notifications (even if notification shade is pulled down)
@ -361,6 +370,12 @@ public class MainApplication extends FoxApplication
// it's a serious bug and a security issue you should report to Google
// Google bug bounties on Android are huge, so you can also get rich by doing that.
options.setAttachScreenshot(true);
// User interaction tracing is not needed to get context of crash
options.setEnableUserInteractionTracing(false);
// Send client reports has nothing to do with error reporting
options.setSendClientReports(false);
// Auto session tracking has nothing to do with error reporting
options.setEnableAutoSessionTracking(false);
// Add a callback that will be used before the event is sent to Sentry.
// With this callback, you can modify the event or, when returning null, also discard the event.
options.setBeforeSend((event, hint) -> {
@ -373,6 +388,11 @@ public class MainApplication extends FoxApplication
stringBuilder.append(cbuf);
}
@Override
public void write(String str) {
stringBuilder.append(str);
}
@Override
public void write(char[] chars, int i, int i1) {
stringBuilder.append(chars, i, i1);
@ -399,10 +419,14 @@ public class MainApplication extends FoxApplication
return event;
} else {
Log.i(TAG, "Blocked sentry report according to user preference");
// We need to do this to avoid crash delay on crash when the event is dropped
DiskFlushNotification diskFlushNotification = hint.getAs(
TypeCheckHint.SENTRY_TYPE_CHECK_HINT, DiskFlushNotification.class);
if (diskFlushNotification != null) diskFlushNotification.markFlushed();
return null;
}
});
});
});*/
}
@Override

@ -32,12 +32,24 @@ public enum NotificationType implements NotificationTypeCst {
return !MainApplication.isShowcaseMode();
}
},
NO_ROOT(R.string.fail_root_magisk, R.drawable.ic_baseline_numbers_24, v -> {
IntentHelper.openUrl(v.getContext(), "https://github.com/topjohnwu/Magisk/blob/master/docs/install.md");
}) {
NO_MAGISK(R.string.fail_magisk_missing, R.drawable.ic_baseline_numbers_24, v ->
IntentHelper.openUrl(v.getContext(),
"https://github.com/topjohnwu/Magisk/blob/master/docs/install.md")) {
@Override
public boolean shouldRemove() {
return InstallerInitializer.getErrorNotification() != this;
}
},
NO_ROOT(R.string.fail_root_magisk, R.drawable.ic_baseline_numbers_24) {
@Override
public boolean shouldRemove() {
return InstallerInitializer.getErrorNotification() != this;
}
},
ROOT_DENIED(R.string.fail_root_denied, R.drawable.ic_baseline_numbers_24) {
@Override
public boolean shouldRemove() {
return InstallerInitializer.peekMagiskPath() != null;
return InstallerInitializer.getErrorNotification() != this;
}
},
MAGISK_OUTDATED(R.string.magisk_outdated, R.drawable.ic_baseline_update_24, v -> {
@ -102,7 +114,7 @@ public enum NotificationType implements NotificationTypeCst {
} else {
IntentHelper.openInstaller(compatActivity, d.getAbsolutePath(),
compatActivity.getString(
R.string.local_install_title), null, null,
R.string.local_install_title), null, null, false,
BuildConfig.DEBUG && // Use debug mode if no root
InstallerInitializer.peekMagiskPath() == null);
}
@ -115,7 +127,7 @@ public enum NotificationType implements NotificationTypeCst {
} else if (s == IntentHelper.RESPONSE_URL) {
IntentHelper.openInstaller(compatActivity, u.toString(),
compatActivity.getString(
R.string.remote_install_title), null, null,
R.string.remote_install_title), null, null, false,
BuildConfig.DEBUG && // Use debug mode if no root
InstallerInitializer.peekMagiskPath() == null);
}

@ -10,6 +10,8 @@ import androidx.annotation.Keep;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoManager;
import java.util.Collection;
/**
* Class made to expose some manager functions to xposed modules.
* It will not be obfuscated on release builds
@ -50,11 +52,16 @@ public class XHooks {
@Keep
public static XRepo addXRepo(String url, String fallbackName) {
return RepoManager.getINSTANCE().addOrGet(url, fallbackName);
return RepoManager.getINSTANCE_UNSAFE().addOrGet(url, fallbackName);
}
@Keep
public static XRepo getXRepo(String url) {
return RepoManager.getINSTANCE().get(url);
return RepoManager.getINSTANCE_UNSAFE().get(url);
}
@Keep
public static Collection<XRepo> getXRepos() {
return RepoManager.getINSTANCE_UNSAFE().getXRepos();
}
}

@ -16,4 +16,7 @@ public abstract class XRepo {
@Keep
public abstract void setEnabled(boolean enabled);
@Keep
public abstract String getName();
}

@ -213,6 +213,7 @@ public final class AndroidacyRepoData extends RepoData {
}
moduleInfo.needRamdisk = jsonObject.optBoolean("needRamdisk", false);
moduleInfo.changeBoot = jsonObject.optBoolean("changeBoot", false);
moduleInfo.mmtReborn = jsonObject.optBoolean("mmtReborn", false);
moduleInfo.support = filterURL(jsonObject.optString("support"));
moduleInfo.donate = filterURL(jsonObject.optString("donate"));
String config = jsonObject.optString("config", "");
@ -226,6 +227,8 @@ public final class AndroidacyRepoData extends RepoData {
RepoModule repoModule = moduleInfoIterator.next();
if (!repoModule.processed) {
moduleInfoIterator.remove();
} else {
repoModule.moduleInfo.verify();
}
}
this.lastUpdate = lastLastUpdate;

@ -23,8 +23,8 @@ import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Hashes;
import com.fox2code.mmm.utils.IntentHelper;
@ -67,9 +67,11 @@ public class AndroidacyWebAPI {
RepoModule repoModule = AndroidacyRepoData
.getInstance().moduleHashMap.get(installTitle);
String title, description;
boolean mmtReborn = false;
if (repoModule != null) {
title = repoModule.moduleInfo.name;
description = repoModule.moduleInfo.description;
mmtReborn = repoModule.moduleInfo.mmtReborn;
if (description == null || description.length() == 0) {
description = this.activity.getString(R.string.no_desc_found);
}
@ -103,16 +105,19 @@ public class AndroidacyWebAPI {
}
final String fModuleUrl = moduleUrl, fTitle = title,
fConfig = config, fChecksum = checksum;
final boolean fMMTReborn = mmtReborn;
builder.setPositiveButton(hasUpdate ?
R.string.update_module : R.string.install_module, (x, y) -> {
IntentHelper.openInstaller(this.activity,
fModuleUrl, fTitle, fConfig, fChecksum);
fModuleUrl, fTitle, fConfig, fChecksum, fMMTReborn);
});
}
builder.setOnCancelListener(dialogInterface -> {
if (!this.activity.backOnResume)
this.consumedAction = false;
});
ExternalHelper.INSTANCE.injectButton(builder,
Uri.parse(moduleUrl), "androidacy_repo");
final int dim5dp = FoxDisplay.dpToPixel(5);
builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp);
this.activity.runOnUiThread(() -> {
@ -247,13 +252,15 @@ public class AndroidacyWebAPI {
RepoModule repoModule = AndroidacyRepoData
.getInstance().moduleHashMap.get(installTitle);
String config = null;
boolean mmtReborn = false;
if (repoModule != null && repoModule.moduleInfo.name.length() >= 3) {
installTitle = repoModule.moduleInfo.name; // Set title to module name
config = repoModule.moduleInfo.config;
mmtReborn = repoModule.moduleInfo.mmtReborn;
}
this.activity.backOnResume = true;
IntentHelper.openInstaller(this.activity,
moduleUrl, installTitle, config, checksum);
moduleUrl, installTitle, config, checksum, mmtReborn);
}
}

@ -14,7 +14,9 @@ public class BackgroundBootListener extends BroadcastReceiver {
if (!BOOT_COMPLETED.equals(intent.getAction())) return;
synchronized (BackgroundUpdateChecker.lock) {
BackgroundUpdateChecker.onMainActivityCreate(context);
BackgroundUpdateChecker.doCheck(context);
if (MainApplication.isBackgroundUpdateCheckEnabled()) {
BackgroundUpdateChecker.doCheck(context);
}
}
}
}

@ -54,28 +54,30 @@ public class BackgroundUpdateChecker extends Worker {
static void doCheck(Context context) {
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
ModuleManager.getINSTANCE().scanAsync();
RepoManager.getINSTANCE().update(null);
ModuleManager.getINSTANCE().scan();
int moduleUpdateCount = 0;
HashMap<String, RepoModule> repoModules =
RepoManager.getINSTANCE().getModules();
for (LocalModuleInfo localModuleInfo :
ModuleManager.getINSTANCE().getModules().values()) {
if ("twrp-keep".equals(localModuleInfo.id)) continue;
RepoModule repoModule = repoModules.get(localModuleInfo.id);
localModuleInfo.checkModuleUpdate();
if (localModuleInfo.updateVersionCode > localModuleInfo.versionCode &&
!PropUtils.isNullString(localModuleInfo.updateVersion)) {
moduleUpdateCount++;
} else if (repoModule != null &&
repoModule.moduleInfo.versionCode > localModuleInfo.versionCode &&
!PropUtils.isNullString(repoModule.moduleInfo.version)) {
moduleUpdateCount++;
ModuleManager.getINSTANCE().runAfterScan(() -> {
int moduleUpdateCount = 0;
HashMap<String, RepoModule> repoModules =
RepoManager.getINSTANCE().getModules();
for (LocalModuleInfo localModuleInfo :
ModuleManager.getINSTANCE().getModules().values()) {
if ("twrp-keep".equals(localModuleInfo.id)) continue;
RepoModule repoModule = repoModules.get(localModuleInfo.id);
localModuleInfo.checkModuleUpdate();
if (localModuleInfo.updateVersionCode > localModuleInfo.versionCode &&
!PropUtils.isNullString(localModuleInfo.updateVersion)) {
moduleUpdateCount++;
} else if (repoModule != null &&
repoModule.moduleInfo.versionCode > localModuleInfo.versionCode &&
!PropUtils.isNullString(repoModule.moduleInfo.version)) {
moduleUpdateCount++;
}
}
}
if (moduleUpdateCount != 0) {
postNotification(context, moduleUpdateCount);
}
if (moduleUpdateCount != 0) {
postNotification(context, moduleUpdateCount);
}
});
}
public static void postNotification(Context context, int updateCount) {

@ -45,6 +45,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
@ -79,6 +80,7 @@ public class InstallerActivity extends FoxActivity {
final String checksum;
final boolean noExtensions;
final boolean rootless;
final boolean mmtReborn;
// Should we allow 3rd part app to install modules?
if (Constants.INTENT_INSTALL_INTERNAL.equals(intent.getAction())) {
if (!MainApplication.checkSecret(intent)) {
@ -93,12 +95,25 @@ public class InstallerActivity extends FoxActivity {
Constants.EXTRA_INSTALL_NO_EXTENSIONS, false);
rootless = intent.getBooleanExtra(// For debug only
Constants.EXTRA_INSTALL_TEST_ROOTLESS, false);
mmtReborn = intent.getBooleanExtra(// For debug only
Constants.EXTRA_INSTALL_MMT_REBORN, false);
} else {
Toast.makeText(this, "Unknown intent!", Toast.LENGTH_SHORT).show();
this.forceBackPressed();
return;
}
Log.i(TAG, "Install link: " + target);
// Note: Sentry only send this info on crash.
/*if (MainApplication.isCrashReportingEnabled()) {
Breadcrumb breadcrumb = new Breadcrumb();
breadcrumb.setType("install");
breadcrumb.setData("target", target);
breadcrumb.setData("name", name);
breadcrumb.setData("checksum", checksum);
breadcrumb.setCategory("app.action.preinstall");
breadcrumb.setLevel(SentryLevel.INFO);
Sentry.addBreadcrumb(breadcrumb);
}*/
boolean urlMode = target.startsWith("http://") || target.startsWith("https://");
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setTitle(name);
@ -121,9 +136,10 @@ public class InstallerActivity extends FoxActivity {
this.rebootFloatingButton = findViewById(R.id.install_terminal_reboot_fab);
this.installerTerminal = new InstallerTerminal(
installTerminal = findViewById(R.id.install_terminal),
this.isLightTheme(), foreground);
this.isLightTheme(), foreground, mmtReborn);
(horizontalScroller != null ? horizontalScroller : installTerminal)
.setBackground(new ColorDrawable(background));
installTerminal.setItemAnimator(null);
this.progressIndicator.setVisibility(View.GONE);
this.progressIndicator.setIndeterminate(true);
this.getWindow().setFlags( // Note: Doesn't require WAKELOCK permission
@ -138,9 +154,10 @@ public class InstallerActivity extends FoxActivity {
!new SuFile(moduleCache.getAbsolutePath()).delete())
Log.e(TAG, "Failed to delete module cache");
String errMessage = "Failed to download module zip";
byte[] rawModule;
try {
Log.i(TAG, (urlMode ? "Downloading: " : "Loading: ") + target);
byte[] rawModule = urlMode ? Http.doHttpGet(target, (progress, max, done) -> {
rawModule = urlMode ? Http.doHttpGet(target, (progress, max, done) -> {
if (max <= 0 && this.progressIndicator.isIndeterminate())
return;
this.runOnUiThread(() -> {
@ -227,6 +244,14 @@ public class InstallerActivity extends FoxActivity {
Log.e(TAG, errMessage, e);
this.setInstallStateFinished(false,
"! " + errMessage, "");
} catch (OutOfMemoryError e) {
//noinspection UnusedAssignment (Important to avoid OutOfMemoryError)
rawModule = null; // Because reference is kept when calling setInstallStateFinished
if ("Failed to install module zip".equals(errMessage))
throw e; // Ignore if in installation state.
Log.e(TAG, "Module too large", e);
this.setInstallStateFinished(false,
"! Module is too large to be loaded on this device", "");
}
}, "Module install Thread").start();
}
@ -281,6 +306,7 @@ public class InstallerActivity extends FoxActivity {
String moduleId = null;
boolean anyKernel3 = false;
boolean magiskModule = false;
boolean mmtReborn = false;
String MAGISK_PATH = InstallerInitializer.peekMagiskPath();
if (MAGISK_PATH == null) {
this.setInstallStateFinished(false, "! Unable to resolve magisk path", "");
@ -313,6 +339,11 @@ public class InstallerActivity extends FoxActivity {
}
ZipEntry moduleProp = zipFile.getEntry("module.prop");
magiskModule = moduleProp != null;
if (zipFile.getEntry("install.sh") == null &&
zipFile.getEntry("customize.sh") == null &&
zipFile.getEntry("setup.sh") != null && magiskModule) {
mmtReborn = true; // MMT-Reborn require a separate runtime
}
moduleId = PropUtils.readModuleId(zipFile
.getInputStream(zipFile.getEntry("module.prop")));
} catch (IOException ignored) {
@ -354,6 +385,7 @@ public class InstallerActivity extends FoxActivity {
}
String installCommand;
File installExecutable;
boolean magiskCmdLine = false;
if (anyKernel3 && moduleId == null) { // AnyKernel zip don't have a moduleId
this.warnReboot = true; // We should probably re-flash magisk...
installExecutable = this.extractInstallScript("anykernel3_installer.sh");
@ -374,6 +406,7 @@ public class InstallerActivity extends FoxActivity {
installCommand = "magisk --install-module \"" + file.getAbsolutePath() + "\"";
installExecutable = new File(MAGISK_PATH.equals("/sbin") ?
"/sbin/magisk" : "/system/bin/magisk");
magiskCmdLine = true;
} else if (moduleId != null) {
installExecutable = this.extractInstallScript("module_installer_compat.sh");
if (installExecutable == null) {
@ -411,11 +444,30 @@ public class InstallerActivity extends FoxActivity {
"export MMM_TEXT_WRAP=" + (this.textWrap ? "1" : "0"),
this.installerTerminal.isAnsiEnabled() ?
AnsiConstants.ANSI_CMD_SUPPORT : "true",
mmtReborn ? "export MMM_MMT_REBORN=1" : "true",
"export BOOTMODE=true", anyKernel3 ? "export AK3TMPFS=" +
InstallerInitializer.peekMagiskPath() + "/ak3tmpfs" :
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
installCommand).to(installerController, installerMonitor);
}
// Note: Sentry only send this info on crash.
/*if (MainApplication.isCrashReportingEnabled()) {
Breadcrumb breadcrumb = new Breadcrumb();
breadcrumb.setType("install");
breadcrumb.setData("moduleId", moduleId == null ? "<null>" : moduleId);
breadcrumb.setData("mmtReborn", mmtReborn ? "true" : "false");
breadcrumb.setData("isAnyKernel3", anyKernel3 ? "true" : "false");
breadcrumb.setData("noExtensions", noExtensions ? "true" : "false");
breadcrumb.setData("magiskCmdLine", magiskCmdLine ? "true" : "false");
breadcrumb.setData("ansi", this.installerTerminal
.isAnsiEnabled() ? "enabled" : "disabled");
breadcrumb.setCategory("app.action.install");
breadcrumb.setLevel(SentryLevel.INFO);
Sentry.addBreadcrumb(breadcrumb);
}*/
if (mmtReborn && magiskCmdLine) {
Log.w(TAG, "mmtReborn and magiskCmdLine may not work well together");
}
}
boolean success = installJob.exec().isSuccess();
// Wait one UI cycle before disabling controller or processing results
@ -641,14 +693,18 @@ public class InstallerActivity extends FoxActivity {
}
}
private static final HashSet<String> extracted = new HashSet<>();
private File extractInstallScript(String script) {
File compatInstallScript = new File(this.moduleCache, script);
if (!compatInstallScript.exists() || compatInstallScript.length() == 0) {
if (!compatInstallScript.exists() || compatInstallScript.length() == 0 ||
!extracted.contains(script)) {
try {
Files.write(compatInstallScript, Files.readAllBytes(
this.getAssets().open(script)));
extracted.add(script);
} catch (IOException e) {
compatInstallScript.delete();
if (compatInstallScript.delete())
extracted.remove(script);
Log.e(TAG, "Failed to extract " + script, e);
return null;
}

@ -4,9 +4,11 @@ import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.NotificationType;
import com.fox2code.mmm.utils.Files;
import com.topjohnwu.superuser.NoShellException;
import com.topjohnwu.superuser.Shell;
@ -16,8 +18,14 @@ import java.util.ArrayList;
public class InstallerInitializer extends Shell.Initializer {
private static final String TAG = "InstallerInitializer";
private static final File MAGISK_SBIN =
new File("/sbin/magisk");
private static final File MAGISK_SYSTEM =
new File("/system/bin/magisk");
private static final File MAGISK_SYSTEM_EX =
new File("/system/xbin/magisk");
private static final boolean HAS_MAGISK = MAGISK_SBIN.exists() ||
MAGISK_SYSTEM.exists() || MAGISK_SYSTEM_EX.exists();
private static String MAGISK_PATH;
private static int MAGISK_VERSION_CODE;
private static boolean HAS_RAMDISK;
@ -33,6 +41,21 @@ public class InstallerInitializer extends Shell.Initializer {
void onFailure(int error);
}
@Nullable
public static NotificationType getErrorNotification() {
Boolean hasRoot = Shell.isAppGrantedRoot();
if (MAGISK_PATH != null &&
hasRoot != Boolean.FALSE) {
return null;
}
if (!HAS_MAGISK) {
return NotificationType.NO_MAGISK;
} else if (hasRoot != Boolean.TRUE) {
return NotificationType.ROOT_DENIED;
}
return NotificationType.NO_ROOT;
}
public static String peekMagiskPath() {
return InstallerInitializer.MAGISK_PATH;
}

@ -19,13 +19,16 @@ public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.Te
private final AnsiContext ansiContext;
private final Object lock = new Object();
private final int foreground;
private final boolean mmtReborn;
private boolean ansiEnabled = false;
public InstallerTerminal(RecyclerView recyclerView, boolean isLightTheme,int foreground) {
public InstallerTerminal(RecyclerView recyclerView, boolean isLightTheme,
int foreground, boolean mmtReborn) {
recyclerView.setLayoutManager(
new LinearLayoutManager(recyclerView.getContext()));
this.recyclerView = recyclerView;
this.foreground = foreground;
this.mmtReborn = mmtReborn;
this.terminal = new ArrayList<>();
this.ansiContext = (isLightTheme ? AnsiContext.LIGHT : AnsiContext.DARK).copy();
this.recyclerView.setAdapter(this);
@ -125,6 +128,13 @@ public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.Te
private ProcessedLine process(String line) {
if (line.isEmpty()) return new ProcessedLine(" ", null);
if (this.mmtReborn) {
if (line.startsWith("- ")) {
line = "[*] " + line.substring(2);
} else if (line.startsWith("! ")) {
line = "[!] " + line.substring(2);
}
}
return new ProcessedLine(line, this.ansiEnabled ?
this.ansiContext.parseAsSpannable(line) : null);
}
@ -141,10 +151,6 @@ public class InstallerTerminal extends RecyclerView.Adapter<InstallerTerminal.Te
itemView.setLines(1);
itemView.setText(" ");
}
private void setText(String text) {
this.textView.setText(text.isEmpty() ? " " : text);
}
}
private static class ProcessedLine {

@ -1,5 +1,8 @@
package com.fox2code.mmm.manager;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.utils.PropUtils;
/**
* Representation of the module.prop
* Optionally flags represent module status
@ -12,11 +15,14 @@ public class ModuleInfo {
public static final int FLAG_MODULE_UNINSTALLING = 0x08;
public static final int FLAG_MODULE_UPDATING_ONLY = 0x10;
public static final int FLAG_MODULE_MAYBE_ACTIVE = 0x20;
public static final int FLAG_MODULE_HAS_ACTIVE_MOUNT = 0x40;
public static final int FLAGS_MODULE_ACTIVE =
FLAG_MODULE_ACTIVE | FLAG_MODULE_MAYBE_ACTIVE;
public static final int FLAG_METADATA_INVALID = 0x80000000;
public static final int FLAG_CUSTOM_INTERNAL = 0x40000000;
private static final int FLAG_FENCE = 0x10000000; // Should never be set
// Magisk standard
public final String id;
@ -28,6 +34,7 @@ public class ModuleInfo {
public String updateJson;
// Community meta
public boolean changeBoot;
public boolean mmtReborn;
public String support;
public String donate;
public String config;
@ -53,6 +60,7 @@ public class ModuleInfo {
this.description = moduleInfo.description;
this.updateJson = moduleInfo.updateJson;
this.changeBoot = moduleInfo.changeBoot;
this.mmtReborn = moduleInfo.mmtReborn;
this.support = moduleInfo.support;
this.donate = moduleInfo.donate;
this.config = moduleInfo.config;
@ -66,4 +74,17 @@ public class ModuleInfo {
public boolean hasFlag(int flag) {
return (this.flags & flag) != 0;
}
public void verify() {
if (BuildConfig.DEBUG) {
if (PropUtils.isNullString(this.name)) {
throw new IllegalArgumentException("name=" +
(name == null ? "null" : "\"" + name + "\""));
}
if ((this.flags & FLAG_FENCE) != 0) {
throw new IllegalArgumentException("flags=" +
Integer.toHexString(this.flags));
}
}
}
}

@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.NoodleDebug;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.SyncManager;
import com.topjohnwu.superuser.Shell;
@ -24,11 +25,12 @@ import java.util.Iterator;
public final class ModuleManager extends SyncManager {
private static final String TAG = "ModuleManager";
// New method is not really effective, this flag force app to use old method
public static final boolean FORCE_NEED_FALLBACK = true;
private static final int FLAG_MM_INVALID = ModuleInfo.FLAG_METADATA_INVALID;
private static final int FLAG_MM_UNPROCESSED = 0x40000000;
private static final int FLAGS_RESET_INIT = FLAG_MM_INVALID |
ModuleInfo.FLAG_MODULE_DISABLED | ModuleInfo.FLAG_MODULE_UPDATING |
ModuleInfo.FLAG_MODULE_UNINSTALLING | ModuleInfo.FLAG_MODULE_ACTIVE;
private static final int FLAG_MM_UNPROCESSED = ModuleInfo.FLAG_CUSTOM_INTERNAL;
private static final int FLAGS_KEEP_INIT = FLAG_MM_UNPROCESSED |
ModuleInfo.FLAGS_MODULE_ACTIVE | ModuleInfo.FLAG_MODULE_UPDATING_ONLY;
private static final int FLAGS_RESET_UPDATE = FLAG_MM_INVALID | FLAG_MM_UNPROCESSED;
private final HashMap<String, LocalModuleInfo> moduleInfos;
private final SharedPreferences bootPrefs;
@ -46,12 +48,13 @@ public final class ModuleManager extends SyncManager {
}
protected void scanInternal(@NonNull UpdateListener updateListener) {
boolean firstBoot = MainApplication.isFirstBoot();
NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug();
noodleDebug.push("Initialize scan");
boolean firstScan = this.bootPrefs.getBoolean("mm_first_scan", true);
SharedPreferences.Editor editor = firstScan ? this.bootPrefs.edit() : null;
for (ModuleInfo v : this.moduleInfos.values()) {
v.flags |= FLAG_MM_UNPROCESSED;
v.flags &= ~FLAGS_RESET_INIT;
v.flags &= FLAGS_KEEP_INIT;
v.name = v.id;
v.version = null;
v.versionCode = 0;
@ -62,38 +65,50 @@ public final class ModuleManager extends SyncManager {
}
String modulesPath = InstallerInitializer.peekModulesPath();
String[] modules = new SuFile("/data/adb/modules").list();
boolean needFallback = modulesPath == null ||
!new SuFile(modulesPath).exists();
if (needFallback) {
boolean needFallback = FORCE_NEED_FALLBACK ||
modulesPath == null || !new SuFile(modulesPath).exists();
if (!FORCE_NEED_FALLBACK && needFallback) {
Log.e(TAG, "Failed to detect modules folder, using fallback instead.");
}
noodleDebug.replace("Scan");
if (modules != null) {
noodleDebug.push("");
for (String module : modules) {
if (!new SuFile("/data/adb/modules/" + module).isDirectory())
continue; // Ignore non directory files inside modules folder
noodleDebug.replace(module);
LocalModuleInfo moduleInfo = moduleInfos.get(module);
if (moduleInfo == null) {
moduleInfo = new LocalModuleInfo(module);
moduleInfos.put(module, moduleInfo);
// Shis should not really happen, but let's handles theses cases anyway
// This should not really happen, but let's handles theses cases anyway
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UPDATING_ONLY;
}
moduleInfo.flags &= ~FLAGS_RESET_UPDATE;
if (new SuFile("/data/adb/modules/" + module + "/disable").exists()) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_DISABLED;
} else if (needFallback && firstScan) {
} else if (firstScan && needFallback) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_ACTIVE;
editor.putBoolean("module_" + moduleInfo.id + "_active", true);
}
if (new SuFile("/data/adb/modules/" + module + "/remove").exists()) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UNINSTALLING;
}
if ((!needFallback && new SuFile(modulesPath, module).exists()) || (!firstBoot
&& bootPrefs.getBoolean("module_" + moduleInfo.id + "_active", false))) {
if ((firstScan && !needFallback && new SuFile(modulesPath, module).exists()) ||
bootPrefs.getBoolean("module_" + moduleInfo.id + "_active", false)) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_ACTIVE;
if (firstScan) {
editor.putBoolean("module_" + moduleInfo.id + "_active", true);
}
} else if (!needFallback) {
moduleInfo.flags &= ~ModuleInfo.FLAG_MODULE_ACTIVE;
}
if ((moduleInfo.flags & ModuleInfo.FLAGS_MODULE_ACTIVE) != 0
&& (new SuFile("/data/adb/modules/" + module + "/system").exists() ||
new SuFile("/data/adb/modules/" + module + "/vendor").exists() ||
new SuFile("/data/adb/modules/" + module + "/zygisk").exists() ||
new SuFile("/data/adb/modules/" + module + "/riru").exists())) {
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT;
}
try {
PropUtils.readProperties(moduleInfo,
@ -103,10 +118,16 @@ public final class ModuleManager extends SyncManager {
moduleInfo.flags |= FLAG_MM_INVALID;
}
}
noodleDebug.pop();
}
noodleDebug.replace("Scan update");
String[] modules_update = new SuFile("/data/adb/modules_update").list();
if (modules_update != null) {
noodleDebug.push("");
for (String module : modules_update) {
if (!new SuFile("/data/adb/modules_update/" + module).isDirectory())
continue; // Ignore non directory files inside modules folder
noodleDebug.replace(module);
LocalModuleInfo moduleInfo = moduleInfos.get(module);
if (moduleInfo == null) {
moduleInfo = new LocalModuleInfo(module);
@ -122,12 +143,16 @@ public final class ModuleManager extends SyncManager {
moduleInfo.flags |= FLAG_MM_INVALID;
}
}
noodleDebug.pop();
}
noodleDebug.replace("Finalize scan");
this.updatableModuleCount = 0;
Iterator<LocalModuleInfo> moduleInfoIterator =
this.moduleInfos.values().iterator();
noodleDebug.push("");
while (moduleInfoIterator.hasNext()) {
LocalModuleInfo moduleInfo = moduleInfoIterator.next();
noodleDebug.replace(moduleInfo.id);
if ((moduleInfo.flags & FLAG_MM_UNPROCESSED) != 0) {
moduleInfoIterator.remove();
continue; // Don't process fallbacks if unreferenced
@ -147,11 +172,14 @@ public final class ModuleManager extends SyncManager {
if (moduleInfo.version == null || moduleInfo.version.trim().isEmpty()) {
moduleInfo.version = "v" + moduleInfo.versionCode;
}
moduleInfo.verify();
}
noodleDebug.pop();
if (firstScan) {
editor.putBoolean("mm_first_scan", false);
editor.apply();
}
noodleDebug.pop();
}
public HashMap<String, LocalModuleInfo> getModules() {
@ -201,7 +229,7 @@ public final class ModuleManager extends SyncManager {
}
public boolean masterClear(ModuleInfo moduleInfo) {
if (moduleInfo.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE)) return false;
if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT)) return false;
String escapedId = moduleInfo.id.replace("\\", "\\\\")
.replace("\"", "\\\"").replace(" ", "\\ ");
try { // Check for module that declare having file outside their own folder.

@ -305,6 +305,8 @@ public class MarkdownActivity extends FoxActivity {
return "12 (S)";
case Build.VERSION_CODES.S_V2:
return "12L";
case Build.VERSION_CODES.TIRAMISU:
return "13 Tiramisu";
default:
return "Sdk: " + version;
}

@ -2,6 +2,7 @@ package com.fox2code.mmm.module;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.text.Spanned;
import android.util.Log;
import android.widget.Button;
@ -20,6 +21,7 @@ import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.IntentHelper;
import com.google.android.material.chip.Chip;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@ -118,9 +120,12 @@ public enum ActionButtonType {
R.string.update_module : R.string.install_module, (x, y) -> {
String updateZipChecksum = moduleHolder.getUpdateZipChecksum();
IntentHelper.openInstaller(button.getContext(), updateZipUrl,
moduleInfo.name, moduleInfo.config, updateZipChecksum);
moduleInfo.name, moduleInfo.config, updateZipChecksum,
moduleInfo.mmtReborn);
});
}
ExternalHelper.INSTANCE.injectButton(builder,
Uri.parse(updateZipUrl), moduleHolder.getUpdateZipRepo());
int dim5dp = FoxDisplay.dpToPixel(5);
builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp);
AlertDialog alertDialog = builder.show();
@ -158,6 +163,7 @@ public enum ActionButtonType {
doActionLong(button, moduleHolder);
return;
}
Log.d("ActionButtonType", Integer.toHexString(moduleHolder.moduleInfo.flags));
if (!ModuleManager.getINSTANCE().setUninstallState(moduleHolder.moduleInfo,
!moduleHolder.hasFlag(ModuleInfo.FLAG_MODULE_UNINSTALLING))) {
Log.e("ActionButtonType", "Failed to switch uninstalled state!");
@ -167,16 +173,19 @@ public enum ActionButtonType {
@Override
public boolean doActionLong(Chip button, ModuleHolder moduleHolder) {
// We can't trust active flag on first boot
if (moduleHolder.moduleInfo.hasFlag(ModuleInfo.FLAGS_MODULE_ACTIVE)) return false;
// Actually a module having mount is the only issue when deleting module
if (moduleHolder.moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_HAS_ACTIVE_MOUNT))
return false; // We can't trust active flag on first boot
new AlertDialog.Builder(button.getContext()).setTitle(R.string.master_delete)
.setPositiveButton(R.string.master_delete_yes, (v, i) -> {
String moduleId = moduleHolder.moduleInfo.id;
if (!ModuleManager.getINSTANCE().masterClear(moduleHolder.moduleInfo)) {
Toast.makeText(button.getContext(), R.string.master_delete_fail,
Toast.LENGTH_SHORT).show();
} else {
moduleHolder.moduleInfo = null;
FoxActivity.getFoxActivity(button).refreshUI();
Log.e("ActionButtonType", "Cleared: " + moduleId);
}
}).setNegativeButton(R.string.master_delete_no, (v, i) -> {
}).create().show();
@ -205,13 +214,15 @@ public enum ActionButtonType {
@Override
public void update(Chip button, ModuleHolder moduleHolder) {
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
button.setChipIcon(button.getContext().getDrawable(supportIconForUrl(moduleInfo.support)));
button.setChipIcon(button.getContext().getDrawable(
supportIconForUrl(moduleInfo.support)));
button.setText(R.string.support);
}
@Override
public void doAction(Chip button, ModuleHolder moduleHolder) {
IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().support);
IntentHelper.openUrl(button.getContext(),
moduleHolder.getMainModuleInfo().support);
}
},
DONATE() {
@ -232,7 +243,9 @@ public enum ActionButtonType {
@DrawableRes
public static int supportIconForUrl(String url) {
int icon = R.drawable.ic_baseline_support_24;
if (url.startsWith("https://t.me/")) {
if (url == null) {
return icon;
} else if (url.startsWith("https://t.me/")) {
icon = R.drawable.ic_baseline_telegram_24;
} else if (url.startsWith("https://discord.gg/") ||
url.startsWith("https://discord.com/invite/")) {
@ -250,8 +263,11 @@ public enum ActionButtonType {
@DrawableRes
public static int donateIconForUrl(String url) {
int icon = R.drawable.ic_baseline_monetization_on_24;
if (url.startsWith("https://www.paypal.me/") ||
url.startsWith("https://www.paypal.com/paypalme/")) {
if (url == null) {
return icon;
} else if (url.startsWith("https://www.paypal.me/") ||
url.startsWith("https://www.paypal.com/paypalme/") ||
url.startsWith("https://www.paypal.com/donate/")) {
icon = R.drawable.ic_baseline_paypal_24;
} else if (url.startsWith("https://patreon.com/") ||
url.startsWith("https://www.patreon.com/")) {

@ -82,6 +82,13 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
this.repoModule.zipUrl : this.moduleInfo.updateZipUrl;
}
public String getUpdateZipRepo() {
return this.moduleInfo == null || (this.repoModule != null &&
this.moduleInfo.updateVersionCode <
this.repoModule.moduleInfo.versionCode) ?
this.repoModule.repoData.id : "update_json";
}
public String getUpdateZipChecksum() {
return this.moduleInfo == null || (this.repoModule != null &&
this.moduleInfo.updateVersionCode <

@ -186,6 +186,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
this.buttonAction.setBackground(null);
LocalModuleInfo localModuleInfo = moduleHolder.moduleInfo;
if (localModuleInfo != null) {
localModuleInfo.verify();
this.switchMaterial.setVisibility(View.VISIBLE);
this.switchMaterial.setChecked((localModuleInfo.flags &
ModuleInfo.FLAG_MODULE_DISABLED) == 0);
@ -198,6 +199,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
this.descriptionText.setVisibility(View.VISIBLE);
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
moduleInfo.verify();
this.titleText.setText(moduleInfo.name);
if (localModuleInfo == null || moduleInfo.versionCode >
localModuleInfo.updateVersionCode) {

@ -14,9 +14,6 @@ import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.module.ModuleHolder;
import com.fox2code.mmm.module.ModuleSorter;
import com.fox2code.mmm.module.ModuleViewAdapter;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.repo.RepoModule;
@ -48,6 +45,10 @@ public class ModuleViewListBuilder {
}
public void addNotification(NotificationType notificationType) {
if (notificationType == null) {
Log.w(TAG, "addNotification(null) called!");
return;
}
synchronized (this.updateLock) {
this.notifications.add(notificationType);
}
@ -113,6 +114,8 @@ public class ModuleViewListBuilder {
public void applyTo(final RecyclerView moduleList,final ModuleViewAdapter moduleViewAdapter) {
if (this.updating) return;
this.updating = true;
ModuleManager.getINSTANCE().afterScan();
RepoManager.getINSTANCE().afterUpdate();
final ArrayList<ModuleHolder> moduleHolders;
final int newNotificationsLen;
final boolean first;

@ -142,6 +142,8 @@ public class RepoData extends XRepo {
if (!repoModule.processed) {
new File(this.cacheRoot, repoModule.id + ".prop").delete();
moduleInfoIterator.remove();
} else {
repoModule.moduleInfo.verify();
}
}
// Update final metadata
@ -220,6 +222,7 @@ public class RepoData extends XRepo {
// Repo data info getters
@NonNull
@Override
public String getName() {
if (isNonNull(this.name))
return this.name;

@ -8,16 +8,19 @@ import androidx.annotation.NonNull;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.XHooks;
import com.fox2code.mmm.XRepo;
import com.fox2code.mmm.androidacy.AndroidacyRepoData;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Hashes;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.NoodleDebug;
import com.fox2code.mmm.utils.PropUtils;
import com.fox2code.mmm.utils.SyncManager;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@ -57,6 +60,23 @@ public final class RepoManager extends SyncManager {
private static volatile RepoManager INSTANCE;
public static RepoManager getINSTANCE() {
if (INSTANCE == null || !INSTANCE.initialized) {
synchronized (lock) {
if (INSTANCE == null) {
MainApplication mainApplication = MainApplication.getINSTANCE();
if (mainApplication != null) {
INSTANCE = new RepoManager(mainApplication);
XHooks.onRepoManagerInitialized();
} else {
throw new RuntimeException("Getting RepoManager too soon!");
}
}
}
}
return INSTANCE;
}
public static RepoManager getINSTANCE_UNSAFE() {
if (INSTANCE == null) {
synchronized (lock) {
if (INSTANCE == null) {
@ -168,6 +188,8 @@ public final class RepoManager extends SyncManager {
private static final double STEP3 = 0.1D;
protected void scanInternal(@NonNull UpdateListener updateListener) {
NoodleDebug noodleDebug = NoodleDebug.getNoodleDebug();
noodleDebug.push("Downloading indexes");
this.modules.clear();
updateListener.update(0D);
// Using LinkedHashSet to deduplicate Androidacy entry.
@ -175,18 +197,26 @@ public final class RepoManager extends SyncManager {
this.repoData.values()).toArray(new RepoData[0]);
RepoUpdater[] repoUpdaters = new RepoUpdater[repoDatas.length];
int moduleToUpdate = 0;
noodleDebug.push("");
for (int i = 0; i < repoDatas.length; i++) {
noodleDebug.replace(repoDatas[i].getName());
moduleToUpdate += (repoUpdaters[i] =
new RepoUpdater(repoDatas[i])).fetchIndex();
updateListener.update(STEP1 / repoDatas.length * (i + 1));
}
noodleDebug.pop();
noodleDebug.replace("Updating meta-data");
int updatedModules = 0;
boolean allowLowQualityModules = MainApplication.isDisableLowQualityModuleFilter();
noodleDebug.push("");
for (int i = 0; i < repoUpdaters.length; i++) {
List<RepoModule> repoModules = repoUpdaters[i].toUpdate();
RepoData repoData = repoDatas[i];
noodleDebug.replace(repoData.getName());
Log.d(TAG, "Registering " + repoData.getName());
noodleDebug.push("");
for (RepoModule repoModule:repoModules) {
noodleDebug.replace(repoModule.id);
try {
if (repoModule.propUrl != null &&
!repoModule.propUrl.isEmpty()) {
@ -212,6 +242,7 @@ public final class RepoManager extends SyncManager {
updatedModules++;
updateListener.update(STEP1 + (STEP2 / moduleToUpdate * updatedModules));
}
noodleDebug.pop();
for (RepoModule repoModule:repoUpdaters[i].toApply()) {
if ((repoModule.moduleInfo.flags & ModuleInfo.FLAG_METADATA_INVALID) == 0) {
RepoModule registeredRepoModule = this.modules.get(repoModule.id);
@ -224,14 +255,20 @@ 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)));
}
noodleDebug.pop();
Log.i(TAG, "Got " + this.modules.size() + " modules!");
updateListener.update(1D);
this.repoLastResult = hasInternet;
noodleDebug.pop(); // pop "Finishing update"
}
public void updateEnabledStates() {
@ -332,4 +369,8 @@ public final class RepoManager extends SyncManager {
public CustomRepoManager getCustomRepoManager() {
return customRepoManager;
}
public Collection<XRepo> getXRepos() {
return new LinkedHashSet<>(this.repoData.values());
}
}

@ -2,6 +2,7 @@ package com.fox2code.mmm.settings;
import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.Application;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
@ -32,6 +33,7 @@ import androidx.preference.TwoStatePreference;
import com.fox2code.foxcompat.FoxActivity;
import com.fox2code.foxcompat.FoxDisplay;
import com.fox2code.foxcompat.FoxViewCompat;
import com.fox2code.foxcompat.internal.FoxProcessExt;
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.Constants;
@ -46,6 +48,7 @@ import com.fox2code.mmm.repo.CustomRepoData;
import com.fox2code.mmm.repo.CustomRepoManager;
import com.fox2code.mmm.repo.RepoData;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.rosettax.LanguageActivity;
@ -67,6 +70,7 @@ import java.util.Random;
public class SettingsActivity extends FoxActivity implements LanguageActivity {
private static final int LANGUAGE_SUPPORT_LEVEL = 1;
private static final String TAG = "SettingsActivity";
private static boolean devModeStepFirstBootIgnore = MainApplication.isDeveloper();
private static int devModeStep = 0;
@Override
@ -88,7 +92,8 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
Intent mStartActivity = new Intent(this, MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
int mPendingIntentId = 123456;
PendingIntent mPendingIntent = PendingIntent.getActivity(this, mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
PendingIntent mPendingIntent = PendingIntent.getActivity(this, mPendingIntentId,
mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
AlarmManager mgr = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
System.exit(0); // Exit app process
@ -115,25 +120,26 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
themePreference.setSummaryProvider(p -> themePreference.getEntry());
themePreference.setOnPreferenceClickListener(p -> {
// You need to reboot your device at least once to be able to access dev-mode
if (!MainApplication.isFirstBoot()) devModeStep = 1;
if (devModeStepFirstBootIgnore || !MainApplication.isFirstBoot()) devModeStep = 1;
return false;
});
themePreference.setOnPreferenceChangeListener((preference, newValue) -> {
devModeStep = 0;
UiThreadHandler.handler.postDelayed(() -> {
MainApplication.getINSTANCE().updateTheme();
FoxActivity.getFoxActivity(this).setThemeRecreate(MainApplication.getINSTANCE().getManagerThemeResId());
FoxActivity.getFoxActivity(this).setThemeRecreate(
MainApplication.getINSTANCE().getManagerThemeResId());
}, 1);
return true;
});
// Crash reporting
TwoStatePreference crashReportingPreference = findPreference("pref_crash_reporting");
/*TwoStatePreference crashReportingPreference = findPreference("pref_crash_reporting");
crashReportingPreference.setChecked(MainApplication.isCrashReportingEnabled());
crashReportingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
devModeStepFirstBootIgnore = true;
devModeStep = 0;
getCrashReportingEditor(requireActivity()).putBoolean("crash_reporting", (boolean) newValue).apply();
return true;
});
});*/
Preference enableBlur = findPreference("pref_enable_blur");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
enableBlur.setSummary(R.string.require_android_6);
@ -204,12 +210,14 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
if (!MainApplication.isDeveloper()) {
findPreference("pref_disable_low_quality_module_filter").setVisible(false);
}
if (!BuildConfig.DEBUG || InstallerInitializer.peekMagiskPath() == null) {
// Hide the pref_crash option if not in debug mode - stop users from purposely crashing the app
Objects.requireNonNull((Preference) findPreference("pref_crash")).setVisible(false);
} else {
findPreference("pref_crash").setOnPreferenceClickListener(preference -> {
// Hard crash the app
throw new RuntimeException("This is a test crash");
throw new Error("This is a test crash");
});
}
if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND || !MainApplication.isDeveloper()) {
@ -217,12 +225,14 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
}
Preference debugNotification = findPreference("pref_background_update_check_debug");
debugNotification.setEnabled(MainApplication.isBackgroundUpdateCheckEnabled());
debugNotification.setVisible(MainApplication.isDeveloper());
debugNotification.setVisible(MainApplication.isDeveloper() && !MainApplication.isWrapped());
debugNotification.setOnPreferenceClickListener(preference -> {
BackgroundUpdateChecker.postNotification(this.requireContext(), new Random().nextInt(4) + 2);
return true;
});
findPreference("pref_background_update_check").setOnPreferenceChangeListener((preference, newValue) -> {
Preference backgroundUpdateCheck = findPreference("pref_background_update_check");
backgroundUpdateCheck.setVisible(!MainApplication.isWrapped());
backgroundUpdateCheck.setOnPreferenceChangeListener((preference, newValue) -> {
boolean enabled = Boolean.parseBoolean(String.valueOf(newValue));
debugNotification.setEnabled(enabled);
if (!enabled) {
@ -242,6 +252,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
if (BuildConfig.DEBUG || BuildConfig.ENABLE_AUTO_UPDATER) {
findPreference("pref_report_bug").setOnPreferenceClickListener(p -> {
devModeStep = 0;
devModeStepFirstBootIgnore = true;
IntentHelper.openUrl(p.getContext(), "https://github.com/Fox2Code/FoxMagiskModuleManager/issues");
return true;
});
@ -260,6 +271,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
Toast.makeText(getContext(), // Tell the user something changed
R.string.dev_mode_enabled, Toast.LENGTH_SHORT).show();
}
ExternalHelper.INSTANCE.refreshHelper(getContext());
return true;
}
IntentHelper.openUrl(p.getContext(), "https://github.com/Fox2Code/FoxMagiskModuleManager");
@ -276,11 +288,26 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
openFragment(libsBuilder.supportFragment(), R.string.licenses);
return true;
});
findPreference("pref_pkg_info").setSummary(BuildConfig.APPLICATION_ID + " v" + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")");
findPreference("pref_pkg_info").setSummary(BuildConfig.APPLICATION_ID +
" v" + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")" +
getRepackageState()); // State may not be "I am just running from myself as myself"
}
private SharedPreferences.Editor getCrashReportingEditor(FragmentActivity requireActivity) {
return requireActivity.getSharedPreferences("crash_reporting", Context.MODE_PRIVATE).edit();
@SuppressLint("RestrictedApi")
private String getRepackageState() {
Application initialApplication = null;
try {
initialApplication = FoxProcessExt.getInitialApplication();
} catch (Throwable ignored) {}
String realPackageName;
if (initialApplication != null) {
realPackageName = initialApplication.getPackageName();
} else {
realPackageName = this.requireContext().getPackageName();
}
if (BuildConfig.APPLICATION_ID.equals(realPackageName)) return "";
return "\n" + this.getString(FoxProcessExt.isRootLoader() ?
R.string.repackaged_as : R.string.wrapped_from) + realPackageName;
}
private void openFragment(Fragment fragment, @StringRes int title) {
@ -300,7 +327,9 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
private int currentLanguageLevel() {
int declaredLanguageLevel = this.getResources().getInteger(R.integer.language_support_level);
if (declaredLanguageLevel != LANGUAGE_SUPPORT_LEVEL) return declaredLanguageLevel;
if (!this.getResources().getConfiguration().locale.getLanguage().equals("en") && this.getResources().getString(R.string.notification_update_pref).equals("Background modules update check") && this.getResources().getString(R.string.notification_update_desc).equals("May increase battery usage")) {
if (!this.getResources().getConfiguration().locale.getLanguage().equals("en") &&
this.getResources().getString(R.string.notification_update_pref).equals("Background modules update check") &&
this.getResources().getString(R.string.notification_update_desc).equals("May increase battery usage")) {
return 0;
}
return LANGUAGE_SUPPORT_LEVEL;
@ -310,7 +339,6 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
public static class RepoFragment extends PreferenceFragmentCompat {
private static final int CUSTOM_REPO_ENTRIES = 5;
@SuppressLint("RestrictedApi")
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
getPreferenceManager().setSharedPreferencesName("mmm");
@ -319,19 +347,24 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
setRepoData(RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT);
setRepoData(RepoManager.DG_MAGISK_REPO_GITHUB);
updateCustomRepoList(true);
onCreatePreferencesAndroidacy();
}
@SuppressLint("RestrictedApi")
public void onCreatePreferencesAndroidacy() {
Preference androidacyTestMode = Objects.requireNonNull(findPreference("pref_androidacy_test_mode"));
if (!MainApplication.isDeveloper()) {
Objects.requireNonNull((Preference) findPreference("pref_androidacy_test_mode")).setVisible(false);
androidacyTestMode.setVisible(false);
} else {
// Show a warning if user tries to enable test mode
Objects.requireNonNull((SwitchPreferenceCompat) findPreference("pref_androidacy_test_mode")).setOnPreferenceChangeListener((preference, newValue) -> {
androidacyTestMode.setOnPreferenceChangeListener((preference, newValue) -> {
if (Boolean.parseBoolean(String.valueOf(newValue))) {
new AlertDialog.Builder(this.requireContext())
.setTitle(R.string.warning)
.setMessage(R.string.androidacy_test_mode_warning)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
// Do nothing
})
.show();
}).show();
}
return true;
});
@ -423,7 +456,8 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
}
Preference preference = findPreference("pref_custom_add_repo");
if (preference == null) return;
preference.setVisible(customRepoManager.canAddRepo() && customRepoManager.getRepoCount() < CUSTOM_REPO_ENTRIES);
preference.setVisible(customRepoManager.canAddRepo() &&
customRepoManager.getRepoCount() < CUSTOM_REPO_ENTRIES);
if (initial) { // Custom repo add button part.
preference = findPreference("pref_custom_add_repo_button");
if (preference == null) return;
@ -470,7 +504,8 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
input.addTextChangedListener(new TextWatcherAdapter() {
@Override
public void onTextChanged(@NonNull CharSequence charSequence, int i, int i1, int i2) {
positiveButton.setEnabled(customRepoManager.canAddRepo(charSequence.toString()) && customRepoManager.getRepoCount() < CUSTOM_REPO_ENTRIES);
positiveButton.setEnabled(customRepoManager.canAddRepo(charSequence.toString()) &&
customRepoManager.getRepoCount() < CUSTOM_REPO_ENTRIES);
}
});
positiveButton.setEnabled(false);

@ -0,0 +1,107 @@
package com.fox2code.mmm.utils;
import android.app.Dialog;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityOptionsCompat;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
import java.util.List;
public final class ExternalHelper {
private static final String TAG = "ExternalHelper";
private static final boolean TEST_MODE = false;
private static final String FOX_MMM_OPEN_EXTERNAL =
"com.fox2code.mmm.utils.intent.action.OPEN_EXTERNAL";
private static final String FOX_MMM_EXTRA_REPO_ID = "extra_repo_id";
public static final ExternalHelper INSTANCE = new ExternalHelper();
private ComponentName fallback;
private CharSequence label;
private boolean multi;
private ExternalHelper() {}
public void refreshHelper(Context context) {
Intent intent = new Intent(FOX_MMM_OPEN_EXTERNAL,
Uri.parse("https://fox2code.com/module.zip"));
List<ResolveInfo> resolveInfos = context.getPackageManager()
.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);
if (resolveInfos == null || resolveInfos.isEmpty()) {
Log.d(TAG, "No external provider installed!");
label = TEST_MODE ? "External" : null;
multi = TEST_MODE;
fallback = null;
} else {
ResolveInfo resolveInfo = resolveInfos.get(0);
Log.d(TAG, "Found external provider: " + resolveInfo.activityInfo.packageName);
fallback = new ComponentName(
resolveInfo.activityInfo.packageName,
resolveInfo.activityInfo.name);
label = resolveInfo.loadLabel(context.getPackageManager());
multi = resolveInfos.size() >= 2;
}
}
public boolean openExternal(Context context, Uri uri, String repoId) {
if (label == null) return false;
Bundle param = ActivityOptionsCompat.makeCustomAnimation(context,
android.R.anim.fade_in, android.R.anim.fade_out).toBundle();
Intent intent = new Intent(FOX_MMM_OPEN_EXTERNAL, uri);
intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId);
if (multi) {
intent = Intent.createChooser(intent, label);
} else {
intent.putExtra(Constants.EXTRA_FADE_OUT, true);
}
try {
if (multi) {
context.startActivity(intent);
} else {
context.startActivity(intent, param);
}
return true;
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Failed to launch activity", e);
}
if (fallback != null) {
if (multi) {
intent = new Intent(FOX_MMM_OPEN_EXTERNAL, uri);
intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId);
intent.putExtra(Constants.EXTRA_FADE_OUT, true);
}
intent.setComponent(fallback);
try {
context.startActivity(intent, param);
return true;
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Failed to launch fallback", e);
}
}
return false;
}
public void injectButton(AlertDialog.Builder builder, Uri uri, String repoId) {
if (label == null) return;
builder.setNeutralButton(label, (dialog, button) -> {
Context context = ((Dialog) dialog).getContext();
if (!openExternal(context, uri, repoId)) {
Toast.makeText(context,
"Failed to launch external activity",
Toast.LENGTH_SHORT).show();
}
});
}
}

@ -2,6 +2,8 @@ package com.fox2code.mmm.utils;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
@ -88,6 +90,35 @@ public class Hashes {
return hash.equals(checksum.toLowerCase(Locale.ROOT));
}
/**
* Check if the checksum match a file by picking the correct
* hashing algorithm depending on the length of the checksum
*/
public static boolean checkSumMatch(InputStream data, String checksum) throws IOException {
String hash;
if (checksum == null) return false;
String checksumAlgorithm = checkSumName(checksum);
if (checksumAlgorithm == null) {
Log.e(TAG, "No hash algorithm for " +
checksum.length() * 8 + "bit checksums");
return false;
}
try {
MessageDigest md = MessageDigest.getInstance(checksumAlgorithm);
byte[] bytes = new byte[2048];
int nRead;
while ((nRead = data.read(bytes)) > 0) {
md.update(bytes, 0, nRead);
}
hash = bytesToHex(md.digest());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
Log.d(TAG, "Checksum result (data: " + hash + ",expected: " + checksum + ")");
return hash.equals(checksum.toLowerCase(Locale.ROOT));
}
public static boolean checkSumValid(String checksum) {
if (checksum == null) return false;
switch (checksum.length()) {

@ -192,7 +192,12 @@ public class IntentHelper {
}
public static void openInstaller(Context context, String url, String title, String config,
String checksum,boolean testDebug) {
String checksum, boolean mmtReborn) {
openInstaller(context, url, title, config, checksum, mmtReborn, false);
}
public static void openInstaller(Context context, String url, String title, String config,
String checksum, boolean mmtReborn, boolean testDebug) {
try {
Intent intent = new Intent(context, InstallerActivity.class);
intent.setAction(Constants.INTENT_INSTALL_INTERNAL);
@ -203,6 +208,8 @@ public class IntentHelper {
intent.putExtra(Constants.EXTRA_INSTALL_CONFIG, config);
if (checksum != null && !checksum.isEmpty())
intent.putExtra(Constants.EXTRA_INSTALL_CHECKSUM, checksum);
if (mmtReborn) // Allow early styling of install process
intent.putExtra(Constants.EXTRA_INSTALL_MMT_REBORN, true);
if (testDebug && BuildConfig.DEBUG)
intent.putExtra(Constants.EXTRA_INSTALL_TEST_ROOTLESS, true);
startActivity(context, intent, true);

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

@ -7,6 +7,7 @@ import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.manager.ModuleInfo;
import com.topjohnwu.superuser.io.SuFileInputStream;
@ -25,6 +26,7 @@ public class PropUtils {
private static final HashMap<String, String> moduleConfigsFallbacks = new HashMap<>();
private static final HashMap<String, Integer> moduleMinApiFallbacks = new HashMap<>();
private static final HashMap<String, String> moduleUpdateJsonFallbacks = new HashMap<>();
private static final HashSet<String> moduleMTTRebornFallback = new HashSet<>();
private static final HashSet<String> moduleImportantProp = new HashSet<>(Arrays.asList(
"id", "name", "version", "versionCode"
));
@ -100,7 +102,8 @@ public class PropUtils {
String name, boolean local) throws IOException {
boolean readId = false, readIdSec = false, readName = false,
readVersionCode = false, readVersion = false, readDescription = false,
readUpdateJson = false, invalid = false, readMinApi = false, readMaxApi = false;
readUpdateJson = false, invalid = false, readMinApi = false, readMaxApi = false,
readMMTReborn = false;
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
@ -203,6 +206,10 @@ public class PropUtils {
case "changeBoot":
moduleInfo.changeBoot = Boolean.parseBoolean(value);
break;
case "mmtReborn":
moduleInfo.mmtReborn = Boolean.parseBoolean(value);
readMMTReborn = true;
break;
case "support":
// Do not accept invalid or too broad support links
if (isInvalidURL(value) ||
@ -304,6 +311,11 @@ public class PropUtils {
if (moduleInfo.config == null) {
moduleInfo.config = moduleConfigsFallbacks.get(moduleInfo.id);
}
if (!readMMTReborn) {
moduleInfo.mmtReborn = moduleMTTRebornFallback.contains(moduleInfo.id) ||
(AppUpdateManager.getFlagsForModule(moduleInfo.id) &
AppUpdateManager.FLAG_COMPAT_MMT_REBORN) != 0;
}
// All local modules should have an author
// set to "Unknown" if author is missing.
if (local && moduleInfo.author == null) {
@ -358,9 +370,8 @@ public class PropUtils {
return moduleInfo == null || moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)
|| moduleInfo.name.length() < 3 || moduleInfo.versionCode < 0
|| moduleInfo.author == null || !TextUtils.isGraphic(moduleInfo.author)
|| (description = moduleInfo.description) == null || !TextUtils.isGraphic(description)
|| isNullString(description = moduleInfo.description) || !TextUtils.isGraphic(description)
|| description.toLowerCase(Locale.ROOT).equals(moduleInfo.name.toLowerCase(Locale.ROOT))
|| description.length() < Math.min(Math.max(moduleInfo.name.length() + 4, 16), 24)
|| (getFlagsForModule(moduleInfo.id) & FLAG_COMPAT_LOW_QUALITY) != 0;
}

@ -13,6 +13,13 @@ public abstract class SyncManager {
private static final UpdateListener NO_OP = value -> {};
protected final Object syncLock = new Object();
private boolean syncing;
private long lastSync;
public final void scanAsync() {
if (!this.syncing) {
new Thread(this::scan, "Scan Thread").start();
}
}
public final void scan() {
this.update(null);
@ -24,10 +31,13 @@ public abstract class SyncManager {
if (!this.syncing) {
// Do scan
synchronized (this.syncLock) {
if (System.currentTimeMillis() < this.lastSync + 50L)
return; // Skip sync if it was synced too recently
this.syncing = true;
try {
this.scanInternal(updateListener);
} finally {
this.lastSync = System.currentTimeMillis();
this.syncing = false;
}
}

@ -51,6 +51,13 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/action_bar_blur" />
<TextView
android:id="@+id/noodle_debug"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/action_bar_blur" />
<LinearLayout
android:id="@+id/search_container"
android:layout_width="match_parent"

@ -27,6 +27,10 @@
<string name="description">Description</string>
<string name="uninstall">Désinstaller</string>
<string name="config">Configuration</string>
<string name="favourite">Favoris</string>
<string name="report_bugs">Rapporter un bug</string>
<string name="sniffed_modules">Modules filtrés</string>
<string name="sniffing_modules">Filtrer modules</string>
<string name="submit_modules">Soumettre un module</string>
<string name="require_android_6">Requière Android 6.0+</string>
<string name="require_android_12">Requière Android 12+</string>
@ -96,6 +100,7 @@
donc j\ai caché cette option derrière le mode développeur, activez-la à vos risques et périls!
</string>
<string name="dev_mode_enabled">Mode développeur activé</string>
<string name="dev_mode_disabled">Mode développeur désactivé</string>
<string name="force_english_pref">Forcer la langue Anglaise</string>
<string name="disable_low_quality_module_filter_pref">Désactiver le filtre de module de basse qualité</string>
<string name="disable_low_quality_module_filter_desc">
@ -127,6 +132,40 @@
<string name="backup_module_list">Sauvegarder les modules</string>
<string name="restore_module_list">Restaurer les modules</string>
<string name="require_internet">Cette opération requière une connexion internet</string>
<string name="crash_reporting_desc">If you disable this, the developer will not get automatic bug reports, and this may make troubleshooting harder</string>
<string name="api_key">Androidacy API Key</string>
<string name="androidacy_test_mode_pref">Mode test Androidacy</string>
<string name="androidacy_test_mode_desc">Utiliser la mise à disposition Androidacy de test au lieu du point de terminaison stable. (Nécessite le redémarrage du processus d\application)</string>
<!-- Background Notification translation -->
<string name="notification_update_title">%i mises à jours de module trouvé</string>
<string name="notification_update_title_easter_egg">%i mises à jours de module camouflée</string>
<string name="notification_update_subtitle">Appuyer pour ouvrir l\application</string>
<string name="notification_update_pref">Vérification de mise à jour pour les modules en arrière-plan</string>
<string name="notification_update_desc">Peut améliorer l\usage de la batterie</string>
<string name="notification_update_debug_pref">Test de notification</string>
<!-- Set to true in translation file if your language is right to left -->
<bool name="lang_support_rtl">false</bool>
<!-- Always copy language_support_level when translating -->
<integer name="language_support_level">1</integer>
<string name="language_support_outdated">Certaines traductions pour la langue actuelle ne sont pas à jour, merci de considérer une contribution aux traudcitons sur Github</string>
<!-- Replace with your own username when translating -->
<string name="language_translated_by">Traduit par xerta555</string>
<string name="crash_reporting">Soumettre automatiquement les bugs et performances auxdéveloppeurs</string>
<string name="crash_reporting_desc">Si vous désactivez ceci, le développeur n\aura pas de rapports de bugs automatiques, et celà peut rendre les corrections plus complexes</string>
<string name="api_key">Clé API Androidacy</string>
<string name="api_key_summary">Utiliser une clé d\API pour Androidacy. Très utile pour les membres prémiums, pour supprimer les pubs et plus encore.</string>
<string name="api_key_not_set">La clé API Androidacyest vide</string>
<string name="api_key_set">Clé API Androidacy actuelle</string>
<string name="api_key_invalid">La clé d\API n\a pas pu être validée. Merci de vérifier et de retenter.</string>
<string name="api_key_valid">Clé API valide.</string>
<string name="checking_api_key">Validation de la clé d\API...</string>
<string name="validating_api_key">Validation de la clé d\API...</string>
<string name="please_wait">Merci de patienter</string>
<string name="api_key_removed">Réinitialisation de la clé API éffectuée avec succès</string>
<string name="save_api_key">Validation</string>
<string name="warning">Avertissement!</string>
<string name="androidacy_test_mode_warning">Vous êtes en train de configurer l\application pour une utilisation non productivepour Androidacy. Celà pourrait conduire à une instabilité ainsi que des échecs à charger le dépôt en ligne. Ne rapportez PAS des bugs si vous avez activé ce paramètrage. Les changements seront effectifs après un redémarrage de l\application.</string>
<string name="crash">Planter l\application pour tester</string>
<string name="repackaged_as">Reconstituer en:</string>
<string name="wrapped_from">Emballé depuis:</string>
</resources>

@ -2,6 +2,8 @@
<string name="app_name">Fox - Menedżer modułów Magisk</string>
<string name="app_name_short">FoxMmm</string>
<string name="fail_root_magisk">Nie udało się uzyskać dostępu do Root lub Magisk</string>
<string name="fail_root_denied">Odmówiono dostępu do roota w aplikacji Magisk</string>
<string name="fail_magisk_missing">Magisk nie jest zainstalowany na tym urządzeniu</string>
<string name="loading">Wczytywanie…</string>
<string name="updatable">Aktualizacje modułów</string>
<string name="installed">Zainstalowane</string>
@ -57,7 +59,7 @@
<string name="showcase_mode_pref">Tryb blokady</string>
<string name="showcase_mode_desc">Tryb blokady uniemożliwia menedżerowi wykonywanie działań na modułach</string>
<string name="prevent_reboot_pref">Uniemożliwiaj restart</string>
<string name="prevent_reboot_desc">Zapobiega niespodziewanym restartom</string>
<string name="prevent_reboot_desc">Zapobiega niespodziewanym restartom urządzenia po instalacji modułu</string>
<string name="pref_category_settings">Ustawienia</string>
<string name="enable_monet">Włącz Monet</string>
<string name="pref_category_info">Informacje</string>
@ -96,6 +98,7 @@
dlatego ukryłem tę opcję za trybem deweloperskim, włącz ją na własne ryzyko!
</string>
<string name="dev_mode_enabled">Tryb programisty włączony</string>
<string name="dev_mode_disabled">Tryb programisty wyłączony</string>
<string name="force_english_pref">Wymuś język angielski</string>
<string name="disable_low_quality_module_filter_pref">Wyłącz filtr modułu niskiej jakości</string>
<string name="disable_low_quality_module_filter_desc">
@ -124,6 +127,7 @@
<string name="add_repo">Dodaj repozytorium</string>
<string name="remove_repo">Usuń repozytorium</string>
<string name="custom_url">Niestandardowy URL</string>
<string name="androidacy_repo_info">To repozytorium może wyświetlać nieinwazyjne reklamy, aby pokryć koszty serwera i rozwoju.</string>
<string name="backup_module_list">Kopia zapasowa modułów</string>
<string name="restore_module_list">Przywracanie modułów</string>
<string name="require_internet">Ta operacja wymaga połączenia internetowego</string>
@ -146,6 +150,22 @@
prosimy o rozważenie uzupełnienia tłumaczeń aplikacji na GitHubie.</string>
<!-- Replace with your own username when translating -->
<string name="language_translated_by">Tłumaczenie: Daviteusz</string>
<string name="crash_reporting_desc">If you disable this, the developer will not get automatic bug reports, and this may make troubleshooting harder</string>
<string name="api_key">Androidacy API Key</string>
<string name="crash_reporting">Automatycznie zgłaszaj błędy i działanie programistom</string>
<string name="crash_reporting_desc">Jeśli to wyłączysz, programista nie będzie otrzymywał automatycznych raportów o błędach, co może utrudnić rozwiązywanie problemów.</string>
<string name="api_key">Klucz API Androidacy</string>
<string name="api_key_summary">Użyj niestandardowego klucza API dla Androidacy. Przydatne dla subskrybentów premium, aby usunąć reklamy i więcej.</string>
<string name="api_key_not_set">Klucz API Androidacy jest pusty</string>
<string name="api_key_set">Bieżący klucz API Androidacy</string>
<string name="api_key_invalid">Nie można zweryfikować klucza API. Proszę sprawdzić i spróbować ponownie.</string>
<string name="api_key_valid">Klucz API jest prawidłowy.</string>
<string name="checking_api_key">Sprawdzanie klucza API...</string>
<string name="validating_api_key">Weryfikacja klucza API...</string>
<string name="please_wait">Proszę czekać</string>
<string name="api_key_removed">Zresetowano klucz API</string>
<string name="save_api_key">Weryfikacja</string>
<string name="warning">Uwaga!</string>
<string name="androidacy_test_mode_warning">Ustawiasz aplikację, aby używać nieprodukcyjnego punktu końcowego dla Androida. Może to spowodować niestabilność aplikacji i brak możliwości załadowania repo online. NIE zgłaszaj błędów, jeśli masz włączony ten przełącznik. Zmiana będzie obowiązywać po ponownym uruchomieniu aplikacji.</string>
<string name="crash">Ubij aplikację do testów</string>
<string name="repackaged_as">Przepakowano jako:</string>
<string name="wrapped_from">Przepakowane z:</string>
</resources>

@ -3,12 +3,13 @@
<string name="app_name">Fox - Manager module Magisk</string>
<string name="app_name_short">Fox\'s Mmm</string>
<string name="fail_root_magisk">Obținerea accesului la rădăcină sau Magisk a eșuat</string>
<string name="fail_root_denied">Accesul root a fost refuzat în aplicația Magisk</string>
<string name="loading">Se încarcă…</string>
<string name="updatable">Actualizabil</string>
<string name="installed">Instalat</string>
<string name="online_repo">Depozit online</string>
<string name="showcase_mode">Aplicația este în modul blocare</string>
<string name="failed_download">Descărcarea fișierului a eșuat.</string>
<string name="failed_download">Descărcarea fișierului a eșuat</string>
<string name="slow_modules">A durat prea mult pentru pornirea modulelor, ia în considerare dezactivarea unor module</string>
<string name="fail_internet">Conectarea la internet a eșuat</string>
<string name="no_web_view">Nu s-a putut obține WebView de sistem</string>
@ -30,8 +31,6 @@
<string name="config">Configurare</string>
<string name="favourite">Favorit</string>
<string name="report_bugs">Raportează erori</string>
<string name="sniffed_modules">Sniffed modules</string>
<string name="sniffing_modules">Sniffing modules</string>
<string name="submit_modules">Trimite un modul</string>
<string name="require_android_6">Necesită Android 6.0+</string>
<string name="require_android_12">Necesită Android 12+</string>
@ -60,11 +59,11 @@
<string name="pref_category_settings">Setări</string>
<string name="enable_monet">Activează Monet</string>
<string name="pref_category_info">Informații</string>
<string name="show_licenses">Afişează licenţa</string>
<string name="show_licenses">Afișează licențe</string>
<string name="licenses">Licențe</string>
<string name="show_incompatible_pref">Arată modulele incompatibile</string>
<string name="show_incompatible_desc">Arată module incompatibile cu dispozitivul tău pe baza metadatelor lor</string>
<string name="magisk_outdated">Magisk este învechit!</string>
<string name="magisk_outdated">Există o nouă versiune Magisk de instalat!</string>
<string name="pref_category_repos">Depozite</string>
<string name="pref_category_security">Securitate</string>
<string name="pref_category_appearance">Aspect</string>
@ -77,7 +76,7 @@
<string name="master_delete_fail">Ștergerea fișierelor modulului a eșuat</string>
<string name="theme_pref">Temă</string>
<string name="theme_mode_pref">Mod temă</string>
<string name="module_id_prefix">ID modul:</string>
<string name="module_id_prefix">ID modul: </string>
<string name="install_from_storage">Instalare modul de pe stocare</string>
<string name="invalid_format">Modulul selectat este într-un format greșit</string>
<string name="low_quality_module">Modul de calitate scăzută</string>
@ -89,7 +88,7 @@
<string name="file_picker_failure">Selectorul de fișiere curent nu a reușit să permită accesul la fișier.</string>
<string name="remote_install_title">Instalare la distanță</string>
<string name="file_picker_wierd">Selectorul tău de fișiere a returnat un răspuns non-standard.</string>
<string name="use_magisk_install_command_pref" tools:ignore="TypographyDashes">Folosește comanda de instalare modul Magisk</string>
<string name="use_magisk_install_command_pref" tools:ignore="TypographyDashes">Folosește comanda \"magisk --install-module\"</string>
<string name="use_magisk_install_command_desc"> În timpul testului, a cauzat probleme la instalarea instrumentului de diagnosticare a erorilor,
așa că am ascuns această opțiune în spatele modului dezvoltator, activați acest lucru pe propriul risc!
</string>
@ -97,19 +96,21 @@
<string name="dev_mode_disabled">Modul dezvoltator este dezactivat</string>
<string name="force_english_pref">Forțează limba engleză</string>
<string name="disable_low_quality_module_filter_pref">Dezactivează filtrul de module de calitate slabă</string>
<string name="disable_low_quality_module_filter_desc"> Unele module nu declară corect metadatele, cauzând erori vizuale,
<string name="disable_low_quality_module_filter_desc">
Unele module nu declară corect metadatele, cauzând erori vizuale,
și/sau indicând calitatea proastă a modulelor, dezactivează pe propriul risc!
</string>
<string name="dns_over_https_pref">Dns prin https</string>
<string name="dns_over_https_pref">DNS prin HTTPS</string>
<string name="dns_over_https_desc"> Poate rezolva problemele de conexiune în unele cazuri.
(Nu se aplică WebView)
(Nu se aplică la WebView)
</string>
<string name="disable_extensions_pref">Dezactivează extensiile</string>
<string name="disable_extensions_desc"> Dezactivează extensiile Fox\'s Mmm, acest lucru împiedică modulele să folosească
extensii terminal, util dacă un modul folosește în mod abuziv extensiile Fox\'s Mmm.
</string>
<string name="wrap_text_pref">Încadrează textul</string>
<string name="wrap_text_desc"> Se încadrează textul la o linie nouă în loc să fie pus
<string name="wrap_text_desc">
Se încadrează textul la o linie nouă în loc să fie pus
tot textul pe aceeași linie atunci când se instalează un modul
</string>
<string name="enable_blur_pref">Activează estomparea</string>
@ -119,14 +120,14 @@
<string name="add_repo">Adăugare depozit</string>
<string name="remove_repo">Eliminare depozit</string>
<string name="custom_url">URL personalizat</string>
<string name="androidacy_repo_info">Acest depozit poate afișa unele reclame neinvazive pentru a acoperi costurile de dezvoltare și pentru server.</string>
<string name="backup_module_list">Rezervare module</string>
<string name="restore_module_list">Restabilire module</string>
<string name="require_internet">Această operațiune necesită o conexiune la Internet</string>
<string name="androidacy_test_mode_pref">Modul de testare Androidacy</string>
<string name="androidacy_test_mode_desc">Use staging Androidacy Endpoint instead of release endpoint. (Require app process restart)</string>
<string name="androidacy_test_mode_desc">Folosiți punctul final Androidacy staging în loc de punctul final de lansare. (Necesită repornirea procesului aplicației)</string>
<!-- Background Notification translation -->
<string name="notification_update_title">S-au găsit %i actualizări module</string>
<string name="notification_update_title_easter_egg">Sniffed %i module updates</string>
<string name="notification_update_subtitle">Clic pentru a deschide aplicația</string>
<string name="notification_update_pref">Verificarea actualizării modulelor în fundal</string>
<string name="notification_update_desc">Poate crește utilizarea bateriei</string>
@ -137,6 +138,22 @@
nu sunt actualizate, vă rugăm să luați în considerare contribuția la traducerea aplicației pe GitHub</string>
<!-- Replace with your own username when translating -->
<string name="language_translated_by">Tradus de Fox2Code</string>
<string name="crash_reporting_desc">If you disable this, the developer will not get automatic bug reports, and this may make troubleshooting harder</string>
<string name="api_key">Androidacy API Key</string>
<string name="crash_reporting">Raportează automat dezvoltatorilor erorile și performanțele</string>
<string name="crash_reporting_desc">Dacă dezactivați acestă opțiune, dezvoltatorul nu va primi rapoarte automate de erori, iar acest lucru poate face depanarea mai dificilă</string>
<string name="api_key">Cheie API Androidacy</string>
<string name="api_key_summary">Folosiți o cheie API personalizată pentru Androidacy. Util pentru abonații premium, pentru a elimina reclamele și multe altele.</string>
<string name="api_key_not_set">Cheia API Androidacy este goală</string>
<string name="api_key_set">Cheie API Androidacy actuală</string>
<string name="api_key_invalid">Nu s-a putut valida cheia API. Vă rugăm verificați-o și încercați din nou.</string>
<string name="api_key_valid">Cheia API este validă.</string>
<string name="checking_api_key">Validare cheie API...</string>
<string name="validating_api_key">Validare cheie API...</string>
<string name="please_wait">Așteptați</string>
<string name="api_key_removed">Resetare cheie API reușită</string>
<string name="save_api_key">Validare</string>
<string name="warning">Avertisment!</string>
<string name="androidacy_test_mode_warning">Setați aplicația să utilizeze un punct final non-producție pentru Androidacy. Acest lucru poate duce la instabilitatea aplicației și la eșecul încărcării depozitului online. NU raportați erori dacă aveți acest comutator pornit. Modificarea va intra în vigoare la repornirea aplicației.</string>
<string name="crash">Oprește forțat aplicația pentru testare</string>
<string name="repackaged_as">Reambalat ca:</string>
<string name="wrapped_from">Înglobat din:</string>
</resources>

@ -2,6 +2,8 @@
<string name="app_name">Fox\'un Magisk Modül Yöneticisi</string>
<string name="app_name_short">Fox\'un Mmy</string>
<string name="fail_root_magisk">Kök ya da Magisk\'e erişim sağlanamadı.</string>
<string name="fail_root_denied">Kök erişimi, Magisk uygulaması aracılığıyla reddedildi</string>
<string name="fail_magisk_missing">Magisk, bu cihazda yüklü değil</string>
<string name="loading">Yükleniyor...</string>
<string name="updatable">Güncellenebilir</string>
<string name="installed">Yüklendi</string>
@ -157,7 +159,7 @@
<string name="api_key_not_set">Androidacy API anahtarı boş</string>
<string name="api_key_set">Mevcut Androidacy API anahtarı</string>
<string name="api_key_invalid">API anahtarı doğrulanamadı. Lütfen kontrol edin ve tekrar deneyin.</string>
<string name="api_key_valid">API key is valid.</string>
<string name="api_key_valid">API anahtarı geçerli.</string>
<string name="checking_api_key">API anahtarı doğrulanıyor...</string>
<string name="validating_api_key">API anahtarı doğrulanıyor...</string>
<string name="please_wait">Lütfen bekleyin</string>
@ -166,4 +168,6 @@
<string name="warning">Uyarı!</string>
<string name="androidacy_test_mode_warning">Uygulamayı Androidacy için üretim dışı bir endpoint kullanmak için ayarlıyorsunuz. Bu, uygulamada kararsızlığa ve çevrimiçi deponun yüklenememesine neden olabilir. Bu anahtar açıksa hataları BİLDİRMEYİN. Değişiklik, uygulama yeniden başlatıldığında etkili olacaktır.</string>
<string name="crash">Test etmek için uygulamayı çökert</string>
<string name="repackaged_as">Bu şekilde yeniden paketlendi:</string>
<string name="wrapped_from">Tarafından sarmalandı:</string>
</resources>

@ -2,6 +2,7 @@
<string name="app_name">Fox\'s Magisk模块管理器</string>
<string name="app_name_short">Fox\'s Mmm</string>
<string name="fail_root_magisk">无法访问Root权限或者您没有安装Magisk</string>
<string name="fail_root_denied">Root被Magisk拒绝</string>
<string name="loading">加载中…</string>
<string name="updatable">可更新</string>
<string name="installed">已安装</string>
@ -59,7 +60,7 @@
<!-- Note: Lockdown mode used to be called showcase mode -->
<string name="manage_repos_pref">管理仓库</string>
<string name="showcase_mode_pref">锁定模式</string>
<string name="showcase_mode_desc">锁定模式阻止管理器对模块执行操作</string>
<string name="showcase_mode_desc">锁定模式阻止管理器对模块执行操作</string>
<string name="prevent_reboot_pref">防止重启</string>
<string name="prevent_reboot_desc">防止误触重启键而导致意外重启</string>
<string name="pref_category_settings">设置</string>
@ -129,9 +130,12 @@
<string name="add_repo">添加库</string>
<string name="remove_repo">移除库</string>
<string name="custom_url">自定义网址</string>
<string name="androidacy_repo_info">此存储库可能会显示一些非侵入性广告以支付服务器和开发成本</string>
<string name="backup_module_list">备份模块</string>
<string name="restore_module_list">还原模块</string>
<string name="require_internet">此操作需要互联网连接</string>
<string name="androidacy_test_mode_pref">Androidacy测试模式</string>
<string name="androidacy_test_mode_desc">使用暂存 Androidacy 端点而不是发布端点 (要求重新启动应用程序进程)</string>
<!-- Background Notification translation -->
<string name="notification_update_title">发现 %i 个模块更新</string>
@ -149,6 +153,22 @@
请考虑为 GitHub 上的应用程序翻译做出贡献</string>
<!-- Replace with your own username when translating -->
<string name="language_translated_by">由ender-zhao翻译</string>
<string name="crash_reporting_desc">If you disable this, the developer will not get automatic bug reports, and this may make troubleshooting harder</string>
<string name="crash_reporting">自动向开发人员报告错误和性能</string>
<string name="crash_reporting_desc">如果禁用此功能,开发人员将不会收到自动错误报告,这可能会使故障排除变得困难</string>
<string name="api_key">Androidacy API Key</string>
<string name="api_key_summary">使用Androidacy的自定义 API key, 对高级订阅者有用,以删除广告等</string>
<string name="api_key_not_set">Androidacy API key 为空</string>
<string name="api_key_set">当前的 Androidacy API key</string>
<string name="api_key_invalid">无法验证 API Key。 请检查并重试。</string>
<string name="api_key_valid">API key有效.</string>
<string name="checking_api_key">正在验证 API key...</string>
<string name="validating_api_key">正在验证 API key...</string>
<string name="please_wait">请等待</string>
<string name="api_key_removed">成功重置 API key</string>
<string name="save_api_key">验证</string>
<string name="warning">警告!</string>
<string name="androidacy_test_mode_warning">您正在将应用设置为使用 Androidacy 的非生产端点。 这可能会导致应用不稳定和无法加载在线 repo。 如果您打开此开关,请勿报告错误。 更改将在应用重启时生效</string>
<string name="crash">使应用程序崩溃以进行测试</string>
<string name="repackaged_as">重新包装为:</string>
<string name="wrapped_from">包来自:</string>
</resources>

@ -2,6 +2,8 @@
<string name="app_name">Fox\'s Magisk Module Manager</string>
<string name="app_name_short">Fox\'s Mmm</string>
<string name="fail_root_magisk">Could not access either Root or Magisk</string>
<string name="fail_root_denied">Root has been denied via the Magisk app</string>
<string name="fail_magisk_missing">Magisk is not installed on this device</string>
<string name="loading">Loading…</string>
<string name="updatable">Upgradable</string>
<string name="installed">Installed</string>
@ -134,7 +136,7 @@
<string name="restore_module_list">Restore modules</string>
<string name="require_internet">This operation require an internet connection</string>
<string name="androidacy_test_mode_pref">Androidacy test mode</string>
<string name="androidacy_test_mode_desc">Use staging Androidacy Endpoint instead of release endpoint. (Require app process restart)</string>
<string name="androidacy_test_mode_desc">Use staging Androidacy endpoint instead of release endpoint. (Require app process restart)</string>
<!-- Background Notification translation -->
<string name="notification_update_title">Found %i module updates</string>
@ -168,4 +170,6 @@
<string name="warning">Warning!</string>
<string name="androidacy_test_mode_warning">You are setting the app to use a non-production endpoint for Androidacy. This may result in app instability and failure to load the online repo. Do NOT report bugs if you have this switch on. Change will take effect on app restart.</string>
<string name="crash">Crash the app for testing</string>
<string name="repackaged_as">Repackaged as:</string>
<string name="wrapped_from">Wrapped from:</string>
</resources>

@ -116,13 +116,13 @@
app:summary="@string/prevent_reboot_desc"
app:title="@string/prevent_reboot_pref" />
<!-- Crash reporting -->
<SwitchPreferenceCompat
<!--<SwitchPreferenceCompat
app:defaultValue="true"
app:icon="@drawable/ic_baseline_bug_report_24"
app:key="pref_crash_reporting"
app:singleLineTitle="false"
app:summary="@string/crash_reporting_desc"
app:title="@string/crash_reporting" />
app:title="@string/crash_reporting" />-->
<!-- Purposely crash the app -->
<Preference
app:icon="@drawable/ic_baseline_bug_report_24"

@ -1,6 +1,6 @@
#Sun Jun 05 10:40:53 EDT 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

Loading…
Cancel
Save