Add MMT-Reborn support + add feedback for known errors + fix bugs.

pull/202/head
Fox2Code 2 years ago
parent c6adf5827f
commit c1cad9b30b

@ -79,6 +79,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 +94,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)

@ -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";

@ -37,7 +37,6 @@ 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,
@ -153,14 +152,16 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
if (!MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
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);
moduleViewListBuilder.addNotification(
InstallerInitializer.getErrorNotification());
this.commonNext();
}
@ -223,7 +224,8 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
setActionBarBackground(null);
updateScreenInsets(getResources().getConfiguration());
});
moduleViewListBuilder.appendRemoteModules();
RepoManager.getINSTANCE().runAfterUpdate(
moduleViewListBuilder::appendRemoteModules);
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
Log.i(TAG, "Finished app opening state!");
}
@ -318,13 +320,15 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
if (!MainApplication.isShowcaseMode())
moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE);
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());
this.commonNext();
}
@ -350,7 +354,8 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe
progressIndicator.setVisibility(View.GONE);
});
}
moduleViewListBuilder.appendRemoteModules();
RepoManager.getINSTANCE().runAfterUpdate(
moduleViewListBuilder::appendRemoteModules);
Log.i(TAG, "Common Before applyTo");
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
Log.i(TAG, "Common After");
@ -393,7 +398,8 @@ 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);
},"Repo update thread").start();
}

@ -37,7 +37,13 @@ public enum NotificationType implements NotificationTypeCst {
}) {
@Override
public boolean shouldRemove() {
return InstallerInitializer.peekMagiskPath() != null;
return InstallerInitializer.getErrorNotification() != this;
}
},
ROOT_DENIED(R.string.fail_root_denied, R.drawable.ic_baseline_numbers_24) {
@Override
public boolean shouldRemove() {
return InstallerInitializer.getErrorNotification() != this;
}
},
MAGISK_OUTDATED(R.string.magisk_outdated, R.drawable.ic_baseline_update_24, v -> {
@ -102,7 +108,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 +121,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);
}

@ -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,7 +23,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.repo.RepoManager;
import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Hashes;
@ -67,9 +66,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,10 +104,11 @@ 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 -> {
@ -247,13 +249,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);
}
}

@ -5,6 +5,8 @@ import android.content.Context;
import android.content.Intent;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.ModuleManager;
public class BackgroundBootListener extends BroadcastReceiver {
private static final String BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED";
@ -14,7 +16,21 @@ 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);
}
}
if (ModuleManager.FORCE_NEED_FALLBACK &&
MainApplication.hasGottenRootAccess()) {
InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
@Override
public void onPathReceived(String path) {
ModuleManager.getINSTANCE().scan();
}
@Override
public void onFailure(int error) {}
});
}
}
}

@ -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;
@ -83,6 +84,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)) {
@ -97,6 +99,8 @@ 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();
@ -136,9 +140,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
@ -305,6 +310,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", "");
@ -337,6 +343,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) {
@ -378,6 +389,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");
@ -398,6 +410,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) {
@ -435,6 +448,7 @@ 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() + "\"",
@ -445,14 +459,19 @@ public class InstallerActivity extends FoxActivity {
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
@ -678,14 +697,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,19 @@ 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 (hasRoot != Boolean.TRUE && HAS_MAGISK) {
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));
}
}
}
}

@ -24,11 +24,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 +47,11 @@ public final class ModuleManager extends SyncManager {
}
protected void scanInternal(@NonNull UpdateListener updateListener) {
boolean firstBoot = MainApplication.isFirstBoot();
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,9 +62,9 @@ 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.");
}
if (modules != null) {
@ -75,25 +75,34 @@ public final class ModuleManager extends SyncManager {
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,
@ -147,6 +156,7 @@ public final class ModuleManager extends SyncManager {
if (moduleInfo.version == null || moduleInfo.version.trim().isEmpty()) {
moduleInfo.version = "v" + moduleInfo.versionCode;
}
moduleInfo.verify();
}
if (firstScan) {
editor.putBoolean("mm_first_scan", false);
@ -201,7 +211,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.

@ -118,7 +118,8 @@ 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);
});
}
int dim5dp = FoxDisplay.dpToPixel(5);
@ -158,6 +159,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 +169,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 +210,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 +239,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,7 +259,9 @@ 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/") ||
if (url == null) {
return icon;
} else if (url.startsWith("https://www.paypal.me/") ||
url.startsWith("https://www.paypal.com/paypalme/")) {
icon = R.drawable.ic_baseline_paypal_24;
} else if (url.startsWith("https://patreon.com/") ||

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

@ -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

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

@ -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) {

@ -13,6 +13,7 @@ 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 scan() {
this.update(null);
@ -24,10 +25,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;
}
}

@ -2,6 +2,7 @@
<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="loading">Loading…</string>
<string name="updatable">Upgradable</string>
<string name="installed">Installed</string>

Loading…
Cancel
Save