0.2.7 Release

pull/32/head 0.2.7
Fox2Code 2 years ago
parent a4ed235f73
commit 584d8b126a

@ -55,7 +55,7 @@ So the original module maker can still override them
The Fox's Mmm also allow better control over it's installer interface
Fox's Mmm defined the variable `MMM_EXT_SUPPORT` to expose it's extension support
Fox's Mmm define the variable `MMM_EXT_SUPPORT` to expose it's extensions support
All the commands start with it `#!`, by default the manager process command as log output
unless `#!useExt` is sent to indicate that the app is ready to use commands
@ -70,15 +70,21 @@ Commands:
- `clearTerminal`: Clear the terminal of any text, making it empty
- `scrollUp`: Scroll up at the top of the terminal
- `scrollDown`: Scroll down at the bottom of the terminal
- `showLoading`: Show an indeterminate progress bar
(Note: the bar is automatically hidden when the install finish)
- `showLoading <max>`: Show an indeterminate progress bar
(Note: Status bar is indeterminate if 0 is provided)
- `setLoading <progress>`: Set loading progress if the bar is not indeterminate.
- `hideLoading`: Hide the indeterminate progress bar if previously shown
- `setSupportLink <url>`: Set support link to show when the install finish
(Note: Modules installed from repo will not show the config button if a link is set)
Variables:
- `MMM_EXT_SUPPORT` declared if extensions are supported
- `MMM_USER_LANGUAGE` the current user selected language
- `MMM_APP_VERSION` display version of the app (Ex: `x.y.z`)
Note:
The current behavior with unknown command is to ignore them,
I may add or remove commands in the future depending of how they are used
I may add or remove commands/variables in the future depending of how they are used
A wrapper script to use theses commands could be
```sh

@ -35,6 +35,8 @@ The app currently use these two repo as it's module sources, with it's benefits
[https://github.com/Magisk-Modules-Repo](https://github.com/Magisk-Modules-Repo)
- No longer accept new modules or update to existing modules
(Fox's MMM use a workaround to get latest version of modules, because the
method used by the official Magisk app give outdated versions of the modules)
- May be shut down at any moment
- Official app dropped support for it
- Officially supported by Fox's mmm

@ -10,8 +10,8 @@ android {
applicationId "com.fox2code.mmm"
minSdk 21
targetSdk 32
versionCode 17
versionName "0.2.6"
versionCode 18
versionName "0.2.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -31,8 +31,7 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
lint {
disable 'MissingTranslation'
}
}

@ -28,6 +28,7 @@
android:theme="@style/Theme.MagiskModuleManager"
android:fullBackupContent="@xml/full_backup_content"
android:dataExtractionRules="@xml/data_extraction_rules"
android:usesCleartextTraffic="false"
tools:targetApi="s">
<receiver android:name="com.fox2code.mmm.manager.ModuleBootReceive"
android:exported="true">
@ -39,7 +40,7 @@
android:name=".settings.SettingsActivity"
android:parentActivityName=".MainActivity"
android:exported="true"
android:label="@string/title_activity_settings" >
android:label="@string/title_activity_settings">
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
</intent-filter>

@ -43,10 +43,12 @@ public enum ActionButtonType {
@Override
public void doAction(ImageButton button, ModuleHolder moduleHolder) {
RepoModule repoModule = moduleHolder.repoModule;
if (repoModule == null) return;
IntentHelper.openInstaller(button.getContext(), repoModule.zipUrl,
repoModule.moduleInfo.name, repoModule.moduleInfo.config);
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
if (moduleInfo == null) return;
String updateZipUrl = moduleHolder.getUpdateZipUrl();
if (updateZipUrl == null) return;
IntentHelper.openInstaller(button.getContext(), updateZipUrl,
moduleInfo.name, moduleInfo.config);
}
},
UNINSTALL() {

@ -41,8 +41,8 @@ public class AppUpdateManager {
return true;
long lastChecked = this.lastChecked;
if (lastChecked != 0 &&
// Avoid spam calls by putting a 10 seconds timer
lastChecked < System.currentTimeMillis() - 10000L)
// Avoid spam calls by putting a 60 seconds timer
lastChecked < System.currentTimeMillis() - 60000L)
return force && this.peekShouldUpdate();
synchronized (this.updateLock) {
if (lastChecked != this.lastChecked)

@ -11,6 +11,8 @@ public class Constants {
public static final String EXTRA_INSTALL_PATH = "extra_install_path";
public static final String EXTRA_INSTALL_NAME = "extra_install_name";
public static final String EXTRA_INSTALL_CONFIG = "extra_install_config";
public static final String EXTRA_INSTALL_NO_PATCH = "extra_install_no_patch";
public static final String EXTRA_INSTALL_NO_EXTENSIONS = "extra_install_no_extensions";
public static final String EXTRA_MARKDOWN_URL = "extra_markdown_url";
public static final String EXTRA_MARKDOWN_TITLE = "extra_markdown_title";
public static final String EXTRA_MARKDOWN_CONFIG = "extra_markdown_config";

@ -18,6 +18,7 @@ import android.widget.TextView;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.settings.SettingsActivity;
@ -115,16 +116,42 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O
progressIndicator.setMax(PRECISION);
});
Log.i(TAG, "Scanning for modules!");
RepoManager.getINSTANCE().update(value -> runOnUiThread(() ->
progressIndicator.setProgressCompat((int) (value * PRECISION), true)));
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 (!RepoManager.getINSTANCE().hasConnectivity()) {
moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
} else {
if (AppUpdateManager.getAppUpdateManager().checkUpdate(true))
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
if (max != 0) {
int current = 0;
for (LocalModuleInfo localModuleInfo :
ModuleManager.getINSTANCE().getModules().values()) {
if (localModuleInfo.updateJson != null) {
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));
}
}
}
}
runOnUiThread(() -> {
progressIndicator.setProgressCompat(PRECISION, true);
progressIndicator.setVisibility(View.GONE);
searchView.setEnabled(true);
});
if (!RepoManager.getINSTANCE().hasConnectivity())
moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
else if (AppUpdateManager.getAppUpdateManager().checkUpdate(true))
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
moduleViewListBuilder.appendRemoteModules();
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter);
Log.i(TAG, "Finished app opening state!");
@ -156,6 +183,7 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O
this.cardIconifyUpdate();
this.moduleViewListBuilder.setQuery(null);
Log.i(TAG, "Item After");
this.moduleViewListBuilder.refreshNotificationsUI(this.moduleViewAdapter);
InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() {
@Override
public void onPathReceived(String path) {

@ -212,10 +212,13 @@ public class MainApplication extends Application implements CompatActivity.Appli
switch (this.managerThemeResId) {
case R.style.Theme_MagiskModuleManager:
this.nightModeOverride = null;
break;
case R.style.Theme_MagiskModuleManager_Light:
this.nightModeOverride = Boolean.FALSE;
break;
case R.style.Theme_MagiskModuleManager_Dark:
this.nightModeOverride = Boolean.TRUE;
break;
default:
}
if (this.markwonThemeContext != null) {
@ -225,6 +228,25 @@ public class MainApplication extends Application implements CompatActivity.Appli
this.markwon = null;
}
public void updateTheme() {
@StyleRes int themeResId;
String theme;
switch (theme = getSharedPreferences().getString("pref_theme", "system")) {
default:
Log.w("MainApplication", "Unknown theme id: " + theme);
case "system":
themeResId = R.style.Theme_MagiskModuleManager;
break;
case "dark":
themeResId = R.style.Theme_MagiskModuleManager_Dark;
break;
case "light":
themeResId = R.style.Theme_MagiskModuleManager_Light;
break;
}
this.setManagerThemeResId(themeResId);
}
@StyleRes
public int getManagerThemeResId() {
return managerThemeResId;
@ -267,20 +289,7 @@ public class MainApplication extends Application implements CompatActivity.Appli
} else {
MainApplication.firstBoot = bootPrefs.getBoolean("first_boot", false);
}
@StyleRes int themeResId;
switch (getSharedPreferences().getString("pref_theme", "system")) {
default:
case "system":
themeResId = R.style.Theme_MagiskModuleManager;
break;
case "dark":
themeResId = R.style.Theme_MagiskModuleManager_Dark;
break;
case "light":
themeResId = R.style.Theme_MagiskModuleManager_Light;
break;
}
this.setManagerThemeResId(themeResId);
this.updateTheme();
// Update SSL Ciphers if update is possible
GMSProviderInstaller.installIfNeeded(this);
// Update emoji config

@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.IntentHelper;
@ -24,7 +25,7 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
public final NotificationType notificationType;
public final Type separator;
public final int footerPx;
public ModuleInfo moduleInfo;
public LocalModuleInfo moduleInfo;
public RepoModule repoModule;
public ModuleHolder(String moduleId) {
@ -60,7 +61,15 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
}
public ModuleInfo getMainModuleInfo() {
return this.repoModule != null ? this.repoModule.moduleInfo : this.moduleInfo;
return this.repoModule != null && (this.moduleInfo == null ||
this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode)
? this.repoModule.moduleInfo : this.moduleInfo;
}
public String getUpdateZipUrl() {
return this.moduleInfo == null || (this.repoModule != null &&
this.moduleInfo.updateVersionCode < this.repoModule.lastUpdated) ?
this.repoModule.zipUrl : this.moduleInfo.updateZipUrl;
}
public String getMainModuleName() {
@ -103,10 +112,9 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
return Type.NOTIFICATION;
} else if (this.moduleInfo == null) {
return Type.INSTALLABLE;
} else if (this.repoModule == null) {
return Type.INSTALLED;
} else if (this.moduleInfo.versionCode <
this.repoModule.moduleInfo.versionCode) {
} else if (this.moduleInfo.versionCode < this.moduleInfo.updateVersionCode ||
(this.repoModule != null && this.moduleInfo.versionCode <
this.repoModule.moduleInfo.versionCode)) {
return Type.UPDATABLE;
} else {
return Type.INSTALLED;
@ -139,7 +147,8 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
if (this.repoModule != null) {
buttonTypeList.add(ActionButtonType.INFO);
}
if (this.repoModule != null && !showcaseMode &&
if ((this.repoModule != null || (this.moduleInfo != null &&
this.moduleInfo.updateZipUrl != null)) && !showcaseMode &&
InstallerInitializer.peekMagiskPath() != null) {
buttonTypeList.add(ActionButtonType.UPDATE_INSTALL);
}

@ -19,6 +19,7 @@ import androidx.annotation.StringRes;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoModule;
@ -164,7 +165,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
boolean showCaseMode = MainApplication.isShowcaseMode();
if (moduleHolder.isModuleHolder()) {
this.buttonAction.setVisibility(View.GONE);
ModuleInfo localModuleInfo = moduleHolder.moduleInfo;
LocalModuleInfo localModuleInfo = moduleHolder.moduleInfo;
if (localModuleInfo != null) {
this.switchMaterial.setVisibility(View.VISIBLE);
this.switchMaterial.setChecked((localModuleInfo.flags &
@ -177,12 +178,27 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();
this.titleText.setText(moduleInfo.name);
this.creditText.setText((localModuleInfo == null ||
moduleInfo.version.equals(localModuleInfo.version) ? moduleInfo.version :
localModuleInfo.version + " (" + this.getString(
R.string.module_last_update) + moduleInfo.version + ")") +
" " + this.getString(R.string.module_by) + " " + moduleInfo.author);
this.descriptionText.setText(moduleInfo.description);
if (localModuleInfo == null || moduleInfo.versionCode >
localModuleInfo.updateVersionCode) {
this.creditText.setText((localModuleInfo == null ||
moduleInfo.version.equals(localModuleInfo.version) ?
moduleInfo.version : localModuleInfo.version + " (" +
this.getString(R.string.module_last_update) +
moduleInfo.version + ")") + " " +
this.getString(R.string.module_by) + " " + moduleInfo.author);
} else {
this.creditText.setText((
localModuleInfo.version.equals(localModuleInfo.updateVersion) ?
localModuleInfo.version : localModuleInfo.version + " (" +
this.getString(R.string.module_last_update) +
localModuleInfo.updateVersion + ")") + " " +
this.getString(R.string.module_by) + " " + localModuleInfo.author);
}
if (moduleInfo.description == null || moduleInfo.description.isEmpty()) {
this.descriptionText.setText(R.string.no_desc_found);
} else {
this.descriptionText.setText(moduleInfo.description);
}
String updateText = moduleHolder.getUpdateTimeText();
if (!updateText.isEmpty()) {
this.updateText.setVisibility(View.VISIBLE);
@ -262,20 +278,32 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
this.cardView.setBackground(this.background);
}
int backgroundAttr = R.attr.colorBackgroundFloating;
int foregroundAttr = R.attr.colorOnBackground;
if (type == ModuleHolder.Type.NOTIFICATION) {
foregroundAttr = moduleHolder.notificationType.foregroundAttr;
backgroundAttr = moduleHolder.notificationType.backgroundAttr;
} else if (type == ModuleHolder.Type.INSTALLED &&
moduleHolder.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
foregroundAttr = R.attr.colorOnError;
backgroundAttr = R.attr.colorError;
}
Resources.Theme theme = this.cardView.getContext().getTheme();
TypedValue value = new TypedValue();
theme.resolveAttribute(backgroundAttr, value, true);
@ColorInt int color = value.data;
@ColorInt int bgColor = value.data;
theme.resolveAttribute(foregroundAttr, value, true);
@ColorInt int fgColor = value.data;
// Fix card background being invisible on light theme
if (color == Color.WHITE) color = 0xFFF8F8F8;
this.cardView.setCardBackgroundColor(color);
if (bgColor == Color.WHITE) bgColor = 0xFFF8F8F8;
this.titleText.setTextColor(fgColor);
this.buttonAction.setColorFilter(fgColor);
this.cardView.setCardBackgroundColor(bgColor);
} else {
Resources.Theme theme = this.titleText.getContext().getTheme();
TypedValue value = new TypedValue();
theme.resolveAttribute(R.attr.colorOnBackground, value, true);
this.buttonAction.setColorFilter(value.data);
this.titleText.setTextColor(value.data);
this.cardView.setBackground(null);
}
if (type == ModuleHolder.Type.FOOTER) {

@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
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;
@ -49,7 +50,7 @@ public class ModuleViewListBuilder {
ModuleManager moduleManager = ModuleManager.getINSTANCE();
moduleManager.runAfterScan(() -> {
Log.i(TAG, "A1: " + moduleManager.getModules().size());
for (ModuleInfo moduleInfo : moduleManager.getModules().values()) {
for (LocalModuleInfo moduleInfo : moduleManager.getModules().values()) {
ModuleHolder moduleHolder = this.mappedModuleHolders.get(moduleInfo.id);
if (moduleHolder == null) {
this.mappedModuleHolders.put(moduleInfo.id,
@ -190,6 +191,12 @@ public class ModuleViewListBuilder {
});
}
public void refreshNotificationsUI(ModuleViewAdapter moduleViewAdapter) {
final int notificationCount = this.notifications.size();
notifySizeChanged(moduleViewAdapter, 0,
notificationCount, notificationCount);
}
private boolean matchFilter(ModuleHolder moduleHolder) {
if (this.query.isEmpty()) return true;
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();

@ -122,7 +122,7 @@ public enum NotificationType implements NotificationTypeCst {
public final boolean special;
NotificationType(@StringRes int textId, int iconId) {
this(textId, iconId, R.attr.colorError, R.attr.colorOnError);
this(textId, iconId, R.attr.colorError, R.attr.colorOnPrimary); //R.attr.colorOnError);
}
NotificationType(@StringRes int textId, int iconId, int backgroundAttr, int foregroundAttr) {

@ -7,6 +7,7 @@ import android.content.ContextWrapper;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -117,8 +118,13 @@ public class CompatActivity extends AppCompatActivity {
}
public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) {
androidx.appcompat.app.ActionBar compatActionBar = this.getSupportActionBar();
androidx.appcompat.app.ActionBar compatActionBar;
try {
compatActionBar = this.getSupportActionBar();
} catch (Exception e) {
Log.e(TAG, "Failed to call getSupportActionBar", e);
compatActionBar = null; // Allow fallback to builtin actionBar.
}
if (compatActionBar != null) {
compatActionBar.setDisplayHomeAsUpEnabled(showHomeAsUp);
} else {
@ -192,7 +198,13 @@ public class CompatActivity extends AppCompatActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
androidx.appcompat.app.ActionBar compatActionBar = this.getSupportActionBar();
androidx.appcompat.app.ActionBar compatActionBar;
try {
compatActionBar = this.getSupportActionBar();
} catch (Exception e) {
Log.e(TAG, "Failed to call getSupportActionBar", e);
compatActionBar = null; // Allow fallback to builtin actionBar.
}
android.app.ActionBar actionBar = this.getActionBar();
if (compatActionBar != null ? (compatActionBar.getDisplayOptions() &
androidx.appcompat.app.ActionBar.DISPLAY_HOME_AS_UP) != 0 :

@ -1,6 +1,5 @@
package com.fox2code.mmm.compat;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;

@ -2,6 +2,7 @@ package com.fox2code.mmm.installer;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
@ -12,10 +13,12 @@ import android.view.WindowManager;
import android.widget.Toast;
import com.fox2code.mmm.ActionButtonType;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.utils.FastException;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
@ -48,6 +51,8 @@ public class InstallerActivity extends CompatActivity {
final Intent intent = this.getIntent();
final String target;
final String name;
final boolean noPatch;
final boolean noExtensions;
// Should we allow 3rd part app to install modules?
if (Constants.INTENT_INSTALL_INTERNAL.equals(intent.getAction())) {
if (!MainApplication.checkSecret(intent)) {
@ -55,13 +60,17 @@ public class InstallerActivity extends CompatActivity {
this.forceBackPressed();
return;
}
target = intent.getExtras().getString(Constants.EXTRA_INSTALL_PATH);
name = intent.getExtras().getString(Constants.EXTRA_INSTALL_NAME);
target = intent.getStringExtra(Constants.EXTRA_INSTALL_PATH);
name = intent.getStringExtra(Constants.EXTRA_INSTALL_NAME);
noPatch = intent.getBooleanExtra(Constants.EXTRA_INSTALL_NO_PATCH, false);
noExtensions = intent.getBooleanExtra(// Allow intent to disable extensions
Constants.EXTRA_INSTALL_NO_EXTENSIONS, false);
} else {
Toast.makeText(this, "Unknown intent!", Toast.LENGTH_SHORT).show();
this.forceBackPressed();
return;
}
Log.i(TAG, "Install link: " + target);
boolean urlMode = target.startsWith("http://") || target.startsWith("https://");
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setTitle(name);
@ -103,23 +112,29 @@ public class InstallerActivity extends CompatActivity {
this.progressIndicator.setProgressCompat(progress, true);
});
});
this.runOnUiThread(() -> {
this.installerTerminal.addLine("- Patching " + name);
this.progressIndicator.setVisibility(View.GONE);
this.progressIndicator.setIndeterminate(true);
});
Log.i(TAG, "Patching: " + moduleCache.getName());
try (OutputStream outputStream = new FileOutputStream(moduleCache)) {
Files.patchModuleSimple(rawModule, outputStream);
outputStream.flush();
} finally {
//noinspection UnusedAssignment (Important for GC)
rawModule = null;
if (noPatch) {
try (OutputStream outputStream = new FileOutputStream(moduleCache)) {
outputStream.write(rawModule);
outputStream.flush();
}
} else {
this.runOnUiThread(() -> {
this.installerTerminal.addLine("- Patching " + name);
this.progressIndicator.setVisibility(View.GONE);
this.progressIndicator.setIndeterminate(true);
});
Log.i(TAG, "Patching: " + moduleCache.getName());
try (OutputStream outputStream = new FileOutputStream(moduleCache)) {
Files.patchModuleSimple(rawModule, outputStream);
outputStream.flush();
}
}
//noinspection UnusedAssignment (Important to avoid OutOfMemoryError)
rawModule = null;
this.runOnUiThread(() -> {
this.installerTerminal.addLine("- Installing " + name);
});
this.doInstall(moduleCache);
this.doInstall(moduleCache, noExtensions);
} catch (IOException e) {
Log.e(TAG, "Failed to download module zip", e);
this.setInstallStateFinished(false,
@ -129,24 +144,36 @@ public class InstallerActivity extends CompatActivity {
} else {
this.installerTerminal.addLine("- Installing " + name);
new Thread(() -> this.doInstall(
this.toDelete = new File(target)),
this.toDelete = new File(target), noExtensions),
"Install Thread").start();
}
}
private void doInstall(File file) {
private void doInstall(File file,boolean noExtensions) {
Log.i(TAG, "Installing: " + moduleCache.getName());
InstallerController installerController = new InstallerController(
this.progressIndicator, this.installerTerminal, file.getAbsoluteFile());
this.progressIndicator, this.installerTerminal,
file.getAbsoluteFile(), noExtensions);
InstallerMonitor installerMonitor;
Shell.Job installJob;
if (MainApplication.isUsingMagiskCommand()) {
if (MainApplication.isUsingMagiskCommand() || noExtensions) {
installerMonitor = new InstallerMonitor(new File(InstallerInitializer
.peekMagiskPath().equals("/sbin") ? "/sbin/magisk" : "/system/bin/magisk"));
installJob = Shell.su("export MMM_EXT_SUPPORT=1",
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
"magisk --install-module \"" + file.getAbsolutePath() + "\"")
.to(installerController, installerMonitor);
if (noExtensions) {
installJob = Shell.su( // No Extensions
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
"magisk --install-module \"" + file.getAbsolutePath() + "\"")
.to(installerController, installerMonitor);
} else {
installJob = Shell.su("export MMM_EXT_SUPPORT=1",
"export MMM_USER_LANGUAGE=" + (MainApplication.isForceEnglish() ?
"en-US" : Resources.getSystem()
.getConfiguration().locale.toLanguageTag()),
"export MMM_APP_VERSION=" + BuildConfig.VERSION_NAME,
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
"magisk --install-module \"" + file.getAbsolutePath() + "\"")
.to(installerController, installerMonitor);
}
} else {
File installScript = this.extractCompatScript();
if (installScript == null) {
@ -156,6 +183,10 @@ public class InstallerActivity extends CompatActivity {
}
installerMonitor = new InstallerMonitor(installScript);
installJob = Shell.su("export MMM_EXT_SUPPORT=1",
"export MMM_USER_LANGUAGE=" + (MainApplication.isForceEnglish() ?
"en-US" : Resources.getSystem()
.getConfiguration().locale.toLanguageTag()),
"export MMM_APP_VERSION=" + BuildConfig.VERSION_NAME,
"cd \"" + this.moduleCache.getAbsolutePath() + "\"",
"sh \"" + installScript.getAbsolutePath() + "\"" +
" /dev/null 1 \"" + file.getAbsolutePath() + "\"")
@ -182,14 +213,17 @@ public class InstallerActivity extends CompatActivity {
private final LinearProgressIndicator progressIndicator;
private final InstallerTerminal terminal;
private final File moduleFile;
private final boolean noExtension;
private boolean enabled, useExt;
private String supportLink = "";
private InstallerController(LinearProgressIndicator progressIndicator,
InstallerTerminal terminal,File moduleFile) {
InstallerTerminal terminal,File moduleFile,
boolean noExtension) {
this.progressIndicator = progressIndicator;
this.terminal = terminal;
this.moduleFile = moduleFile;
this.noExtension = noExtension;
this.enabled = true;
this.useExt = false;
}
@ -198,7 +232,7 @@ public class InstallerActivity extends CompatActivity {
public void onAddElement(String s) {
if (!this.enabled) return;
Log.d(TAG, "MSG: " + s);
if ("#!useExt".equals(s)) {
if ("#!useExt".equals(s.trim()) && !this.noExtension) {
this.useExt = true;
return;
}
@ -216,7 +250,7 @@ public class InstallerActivity extends CompatActivity {
final String command;
int i = rawCommand.indexOf(' ');
if (i != -1) {
arg = rawCommand.substring(i + 1);
arg = rawCommand.substring(i + 1).trim();
command = rawCommand.substring(2, i);
} else {
arg = "";
@ -239,8 +273,36 @@ public class InstallerActivity extends CompatActivity {
this.terminal.scrollDown();
break;
case "showLoading":
if (!arg.isEmpty()) {
try {
short s = Short.parseShort(arg);
if (s <= 0) throw FastException.INSTANCE;
this.progressIndicator.setMax(s);
this.progressIndicator.setIndeterminate(false);
} catch (Exception ignored) {
this.progressIndicator.setProgressCompat(0, true);
this.progressIndicator.setMax(100);
if (this.progressIndicator.getVisibility() == View.VISIBLE) {
this.progressIndicator.setVisibility(View.GONE);
}
this.progressIndicator.setIndeterminate(true);
}
} else if (!rawCommand.trim().equals("#!showLoading")) {
this.progressIndicator.setProgressCompat(0, true);
this.progressIndicator.setMax(100);
if (this.progressIndicator.getVisibility() == View.VISIBLE) {
this.progressIndicator.setVisibility(View.GONE);
}
this.progressIndicator.setIndeterminate(true);
}
this.progressIndicator.setVisibility(View.VISIBLE);
break;
case "setLoading":
try {
this.progressIndicator.setProgressCompat(
Short.parseShort(arg), true);
} catch (Exception ignored) {}
break;
case "hideLoading":
this.progressIndicator.setVisibility(View.GONE);
break;

@ -1,8 +1,6 @@
package com.fox2code.mmm.installer;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.view.ViewGroup;
import android.widget.TextView;

@ -0,0 +1,45 @@
package com.fox2code.mmm.manager;
import android.util.Log;
import com.fox2code.mmm.utils.FastException;
import com.fox2code.mmm.utils.Http;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
public class LocalModuleInfo extends ModuleInfo {
public String updateVersion;
public long updateVersionCode = Long.MIN_VALUE;
public String updateZipUrl;
public String updateChangeLog;
public LocalModuleInfo(String id) {
super(id);
}
public void checkModuleUpdate() {
if (this.updateJson != null) {
try {
JSONObject jsonUpdate = new JSONObject(new String(Http.doHttpGet(
this.updateJson, false), StandardCharsets.UTF_8));
this.updateVersion = jsonUpdate.optString("version");
this.updateVersionCode = jsonUpdate.getLong("versionCode");
this.updateZipUrl = jsonUpdate.getString("zipUrl");
this.updateChangeLog = jsonUpdate.optString("changelog");
if (this.updateZipUrl.isEmpty()) throw FastException.INSTANCE;
if (this.updateVersion == null || this.updateVersion.trim().isEmpty()) {
this.updateVersion = "v" + this.updateVersionCode;
}
} catch (Exception e) {
this.updateVersion = null;
this.updateVersionCode = Long.MIN_VALUE;
this.updateZipUrl = null;
this.updateChangeLog = null;
Log.w("LocalModuleInfo",
"Failed update checking for module: " + this.id, e);
}
}
}
}

@ -21,6 +21,7 @@ public class ModuleInfo {
public long versionCode;
public String author;
public String description;
public String updateJson;
// Community meta
public String support;
public String donate;
@ -37,6 +38,23 @@ public class ModuleInfo {
this.name = id;
}
public ModuleInfo(ModuleInfo moduleInfo) {
this.id = moduleInfo.id;
this.name = moduleInfo.name;
this.version = moduleInfo.version;
this.versionCode = moduleInfo.versionCode;
this.author = moduleInfo.author;
this.description = moduleInfo.description;
this.updateJson = moduleInfo.updateJson;
this.support = moduleInfo.support;
this.donate = moduleInfo.donate;
this.config = moduleInfo.config;
this.minMagisk = moduleInfo.minMagisk;
this.minApi = moduleInfo.minApi;
this.maxApi = moduleInfo.maxApi;
this.flags = moduleInfo.flags;
}
public boolean hasFlag(int flag) {
return (this.flags & flag) != 0;
}

@ -4,10 +4,19 @@ import android.content.SharedPreferences;
import android.util.Log;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.PropUtils;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
@ -20,9 +29,10 @@ public final class ModuleManager {
ModuleInfo.FLAG_MODULE_DISABLED | ModuleInfo.FLAG_MODULE_UPDATING |
ModuleInfo.FLAG_MODULE_UNINSTALLING | ModuleInfo.FLAG_MODULE_ACTIVE;
private static final int FLAGS_RESET_UPDATE = FLAG_MM_INVALID | FLAG_MM_UNPROCESSED;
private final HashMap<String, ModuleInfo> moduleInfos;
private final HashMap<String, LocalModuleInfo> moduleInfos;
private final SharedPreferences bootPrefs;
private final Object scanLock = new Object();
private int updatableModuleCount = 0;
private boolean scanning;
private static final ModuleManager INSTANCE = new ModuleManager();
@ -75,16 +85,18 @@ public final class ModuleManager {
v.version = null;
v.versionCode = 0;
v.author = null;
v.description = "No description found.";
v.description = "";
v.support = null;
v.config = null;
}
String[] modules = new SuFile("/data/adb/modules").list();
if (modules != null) {
for (String module : modules) {
ModuleInfo moduleInfo = moduleInfos.get(module);
if (!new SuFile("/data/adb/modules/" + module).isDirectory())
continue; // Ignore non directory files inside modules folder
LocalModuleInfo moduleInfo = moduleInfos.get(module);
if (moduleInfo == null) {
moduleInfo = new ModuleInfo(module);
moduleInfo = new LocalModuleInfo(module);
moduleInfos.put(module, moduleInfo);
// Shis should not really happen, but let's handles theses cases anyway
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UPDATING_ONLY;
@ -119,9 +131,9 @@ public final class ModuleManager {
String[] modules_update = new SuFile("/data/adb/modules_update").list();
if (modules_update != null) {
for (String module : modules_update) {
ModuleInfo moduleInfo = moduleInfos.get(module);
LocalModuleInfo moduleInfo = moduleInfos.get(module);
if (moduleInfo == null) {
moduleInfo = new ModuleInfo(module);
moduleInfo = new LocalModuleInfo(module);
moduleInfos.put(module, moduleInfo);
}
moduleInfo.flags &= ~FLAGS_RESET_UPDATE;
@ -135,19 +147,28 @@ public final class ModuleManager {
}
}
}
Iterator<ModuleInfo> moduleInfoIterator =
this.updatableModuleCount = 0;
Iterator<LocalModuleInfo> moduleInfoIterator =
this.moduleInfos.values().iterator();
while (moduleInfoIterator.hasNext()) {
ModuleInfo moduleInfo = moduleInfoIterator.next();
LocalModuleInfo moduleInfo = moduleInfoIterator.next();
if ((moduleInfo.flags & FLAG_MM_UNPROCESSED) != 0) {
moduleInfoIterator.remove();
continue; // Don't process fallbacks if unreferenced
}
if (moduleInfo.updateJson != null) {
this.updatableModuleCount++;
} else {
moduleInfo.updateVersion = null;
moduleInfo.updateVersionCode = Long.MIN_VALUE;
moduleInfo.updateZipUrl = null;
moduleInfo.updateChangeLog = null;
}
if (moduleInfo.name == null || (moduleInfo.name.equals(moduleInfo.id))) {
moduleInfo.name = Character.toUpperCase(moduleInfo.id.charAt(0)) +
moduleInfo.id.substring(1).replace('_', ' ');
}
if (moduleInfo.version == null) {
if (moduleInfo.version == null || moduleInfo.version.trim().isEmpty()) {
moduleInfo.version = "v" + moduleInfo.versionCode;
}
}
@ -157,11 +178,16 @@ public final class ModuleManager {
}
}
public HashMap<String, ModuleInfo> getModules() {
public HashMap<String, LocalModuleInfo> getModules() {
this.afterScan();
return this.moduleInfos;
}
public int getUpdatableModuleCount() {
this.afterScan();
return this.updatableModuleCount;
}
public boolean setEnabledState(ModuleInfo moduleInfo, boolean checked) {
if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING) && !checked) return false;
SuFile disable = new SuFile("/data/adb/modules/" + moduleInfo.id + "/disable");
@ -200,8 +226,28 @@ public final class ModuleManager {
public boolean masterClear(ModuleInfo moduleInfo) {
if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_ACTIVE)) return false;
Shell.su("rm -rf /data/adb/modules/" + moduleInfo.id + "/").exec();
Shell.su("rm -rf /data/adb/modules_update/" + moduleInfo.id + "/").exec();
String escapedId = moduleInfo.id.replace("\\", "\\\\")
.replace("\"", "\\\"").replace(" ", "\\ ");
try { // Check for module that declare having file outside their own folder.
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
SuFileInputStream.open("/data/adb/modules/." + moduleInfo.id + "-files"),
StandardCharsets.UTF_8))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
line = line.trim().replace(' ', '.');
if (!line.startsWith("/data/adb/") || line.contains("*") ||
line.contains("/../") || line.endsWith("/..") ||
line.startsWith("/data/adb/modules") ||
line.equals("/data/adb/magisk.db")) continue;
line = line.replace("\\", "\\\\")
.replace("\"", "\\\"");
Shell.su("rm -rf \"" + line + "\"").exec();
}
}
} catch (IOException ignored) {}
Shell.su("rm -rf /data/adb/modules/" + escapedId + "/").exec();
Shell.su("rm -f /data/adb/modules/." + escapedId + "-files").exec();
Shell.su("rm -rf /data/adb/modules_update/" + escapedId + "/").exec();
moduleInfo.flags = ModuleInfo.FLAG_METADATA_INVALID;
return true;
}

@ -1,10 +1,12 @@
package com.fox2code.mmm.repo;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.PropUtils;
import org.json.JSONArray;
@ -12,11 +14,18 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class RepoData {
private final Object populateLock = new Object();
@ -24,30 +33,64 @@ public class RepoData {
public final File cacheRoot;
public final SharedPreferences cachedPreferences;
public final File metaDataCache;
public final boolean special;
public final HashMap<String, RepoModule> moduleHashMap;
public long lastUpdate;
public String name;
private final Map<String, Long> specialTimes;
private long specialLastUpdate;
RepoData(String url, File cacheRoot, SharedPreferences cachedPreferences) {
this(url, cacheRoot, cachedPreferences, false);
}
RepoData(String url, File cacheRoot, SharedPreferences cachedPreferences,boolean special) {
this.url = url;
this.cacheRoot = cacheRoot;
this.cachedPreferences = cachedPreferences;
this.metaDataCache = new File(cacheRoot, "modules.json");
this.special = special;
this.moduleHashMap = new HashMap<>();
this.name = this.url; // Set url as default name
this.specialTimes = special ? new HashMap<>() : Collections.emptyMap();
if (!this.cacheRoot.isDirectory()) {
this.cacheRoot.mkdirs();
} else if (this.metaDataCache.exists()) {
try {
List<RepoModule> modules = this.populate(new JSONObject(
new String(Files.read(this.metaDataCache), StandardCharsets.UTF_8)));
for (RepoModule repoModule: modules) {
if (!this.tryLoadMetadata(repoModule)) {
repoModule.moduleInfo.flags &=~ ModuleInfo.FLAG_METADATA_INVALID;
} else {
if (special) { // Special times need to be loaded before populate
File metaDataCacheSpecial = new File(cacheRoot, "modules_times.json");
if (metaDataCacheSpecial.exists()) {
try {
JSONArray jsonArray = new JSONArray(new String(
Files.read(this.metaDataCache), StandardCharsets.UTF_8));
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
this.specialTimes.put(jsonObject.getString("name"),
Objects.requireNonNull(ISO_OFFSET_DATE_TIME.parse(
jsonObject.getString("pushed_at"))).getTime());
Log.d("RepoData", "Got " +
jsonObject.getString("name") + " from local storage!");
}
this.specialLastUpdate = metaDataCacheSpecial.lastModified();
if (this.specialLastUpdate > System.currentTimeMillis()) {
this.specialLastUpdate = 0; // Don't allow time travel
}
} catch (Exception e) {
metaDataCacheSpecial.delete();
}
}
} catch (Exception e) {
this.metaDataCache.delete();
}
if (this.metaDataCache.exists()) {
try {
List<RepoModule> modules = this.populate(new JSONObject(
new String(Files.read(this.metaDataCache), StandardCharsets.UTF_8)));
for (RepoModule repoModule : modules) {
if (!this.tryLoadMetadata(repoModule)) {
repoModule.moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID;
}
}
} catch (Exception e) {
this.metaDataCache.delete();
}
}
}
}
@ -62,6 +105,7 @@ public class RepoData {
for (RepoModule repoModule : this.moduleHashMap.values()) {
repoModule.processed = false;
}
Log.d("RepoData", "Data: " + this.specialTimes.toString());
JSONArray array = jsonObject.getJSONArray("modules");
int len = array.length();
for (int i = 0; i < len; i++) {
@ -69,10 +113,23 @@ public class RepoData {
String moduleId = module.getString("id");
// Deny remote modules ids shorter than 3 chars long or that start with a digit
if (moduleId.length() < 3 || Character.isDigit(moduleId.charAt(0))) continue;
Long moduleLastUpdateSpecial = this.specialTimes.get(moduleId);
long moduleLastUpdate = module.getLong("last_update");
String moduleNotesUrl = module.getString("notes_url");
String modulePropsUrl = module.getString("prop_url");
String moduleZipUrl = module.getString("zip_url");
if (moduleLastUpdateSpecial != null) { // Fix last update time
Log.d("RepoData", "Data: " + moduleLastUpdate + " -> " +
moduleLastUpdateSpecial + " for " + moduleId);
moduleLastUpdate = Math.max(moduleLastUpdate, moduleLastUpdateSpecial);
moduleNotesUrl = Http.updateLink(moduleNotesUrl);
modulePropsUrl = Http.updateLink(modulePropsUrl);
moduleZipUrl = Http.updateLink(moduleZipUrl);
} else {
moduleNotesUrl = Http.fixUpLink(moduleNotesUrl);
modulePropsUrl = Http.fixUpLink(modulePropsUrl);
moduleZipUrl = Http.fixUpLink(moduleZipUrl);
}
RepoModule repoModule = this.moduleHashMap.get(moduleId);
if (repoModule == null) {
repoModule = new RepoModule(moduleId);
@ -107,12 +164,17 @@ public class RepoData {
return newModules;
}
public void storeMetadata(RepoModule repoModule,byte[] data) throws IOException {
Files.write(new File(this.cacheRoot, repoModule.id + ".prop"), data);
}
public boolean tryLoadMetadata(RepoModule repoModule) {
File file = new File(this.cacheRoot, repoModule.id + ".prop");
if (file.exists()) {
try {
ModuleInfo moduleInfo = repoModule.moduleInfo;
PropUtils.readProperties(moduleInfo, file.getAbsolutePath(), false);
PropUtils.readProperties(moduleInfo, file.getAbsolutePath(),
repoModule.repoName + "/" + moduleInfo.name, false);
moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID;
if (moduleInfo.version == null) {
moduleInfo.version = "v" + moduleInfo.versionCode;
@ -126,6 +188,37 @@ public class RepoData {
return false;
}
@SuppressLint("SimpleDateFormat")
private static final SimpleDateFormat ISO_OFFSET_DATE_TIME =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
public void updateSpecialTimes(boolean force) throws IOException, JSONException {
if (!this.special) return;
synchronized (this.populateLock) {
if (this.specialLastUpdate == 0L ||
(force && this.specialLastUpdate < System.currentTimeMillis() - 60000L)) {
File metaDataCacheSpecial = new File(cacheRoot, "modules_times.json");
this.specialTimes.clear();
try {
byte[] data = Http.doHttpGet(
"https://api.github.com/users/Magisk-Modules-Repo/repos",
false);
JSONArray jsonArray = new JSONArray(new String(data, StandardCharsets.UTF_8));
for (int i = 0;i < jsonArray.length();i++) {
JSONObject jsonObject = jsonArray.optJSONObject(i);
this.specialTimes.put(jsonObject.getString("name"),
Objects.requireNonNull(ISO_OFFSET_DATE_TIME.parse(
jsonObject.getString("pushed_at"))).getTime());
}
Files.write(metaDataCacheSpecial, data);
this.specialLastUpdate = System.currentTimeMillis();
} catch (ParseException e) {
throw new IOException(e);
}
}
}
}
public String getNameOrFallback(String fallback) {
return this.name == null ||
this.name.equals(this.url) ?

@ -22,11 +22,17 @@ public final class RepoManager {
"https://magisk-modules-repo.github.io/submission/modules.json";
public static final String MAGISK_REPO =
"https://raw.githubusercontent.com/Magisk-Modules-Repo/submission/modules/modules.json";
public static final String MAGISK_REPO_JSDELIVR =
"https://cdn.jsdelivr.net/gh/Magisk-Modules-Repo/submission@modules/modules.json";
public static final String MAGISK_ALT_REPO =
"https://raw.githubusercontent.com/Magisk-Modules-Alt-Repo/json/main/modules.json";
public static final String MAGISK_ALT_REPO_JSDELIVR =
"https://cdn.jsdelivr.net/gh/Magisk-Modules-Alt-Repo/json@main/modules.json";
public static final String MAGISK_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Repo";
public static final String MAGISK_ALT_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Alt-Repo";
public static final String MAGISK_REPO_HOMEPAGE =
"https://github.com/Magisk-Modules-Repo";
public static final String MAGISK_ALT_REPO_HOMEPAGE =
"https://github.com/Magisk-Modules-Alt-Repo";
private static final Object lock = new Object();
private static RepoManager INSTANCE;
@ -56,8 +62,8 @@ public final class RepoManager {
this.repoData = new LinkedHashMap<>();
this.modules = new HashMap<>();
// We do not have repo list config yet.
this.addRepoData(MAGISK_REPO);
this.addRepoData(MAGISK_ALT_REPO);
this.addRepoData(MAGISK_REPO_JSDELIVR);
this.addRepoData(MAGISK_ALT_REPO_JSDELIVR);
// Populate default cache
for (RepoData repoData:this.repoData.values()) {
for (RepoModule repoModule:repoData.moduleHashMap.values()) {
@ -77,6 +83,14 @@ public final class RepoManager {
}
public RepoData get(String url) {
switch (url) {
case MAGISK_REPO_MANAGER:
case MAGISK_REPO:
url = MAGISK_REPO_JSDELIVR;
break;
case MAGISK_ALT_REPO:
url = MAGISK_ALT_REPO_JSDELIVR;
}
return this.repoData.get(url);
}
@ -154,6 +168,8 @@ public final class RepoManager {
RepoData repoData = repoDatas[i];
for (RepoModule repoModule:repoModules) {
try {
repoData.storeMetadata(repoModule,
Http.doHttpGet(repoModule.propUrl, false));
Files.write(new File(repoData.cacheRoot, repoModule.id + ".prop"),
Http.doHttpGet(repoModule.propUrl, false));
if (repoDatas[i].tryLoadMetadata(repoModule) && (allowLowQualityModules ||
@ -208,8 +224,10 @@ public final class RepoManager {
switch (url) {
case MAGISK_REPO_MANAGER:
case MAGISK_REPO:
case MAGISK_REPO_JSDELIVR:
return "magisk_repo";
case MAGISK_ALT_REPO:
case MAGISK_ALT_REPO_JSDELIVR:
return "magisk_alt_repo";
default:
return "repo_" + Hashes.hashSha1(url);
@ -221,7 +239,8 @@ public final class RepoManager {
File cacheRoot = new File(this.mainApplication.getCacheDir(), id);
SharedPreferences sharedPreferences = this.mainApplication
.getSharedPreferences("mmm_" + id, Context.MODE_PRIVATE);
RepoData repoData = new RepoData(url, cacheRoot, sharedPreferences);
RepoData repoData = new RepoData(url, cacheRoot,
sharedPreferences, id.equals("magisk_repo"));
this.repoData.put(url, repoData);
return repoData;
}

@ -28,6 +28,7 @@ public class RepoUpdater {
public int fetchIndex() {
try {
this.indexRaw = Http.doHttpGet(this.repoData.url, false);
if (this.repoData.special) this.repoData.updateSpecialTimes(true);
this.toUpdate = this.repoData.populate(new JSONObject(
new String(this.indexRaw, StandardCharsets.UTF_8)));
// Since we reuse instances this should work

@ -59,21 +59,11 @@ public class SettingsActivity extends CompatActivity {
});
themePreference.setOnPreferenceChangeListener((preference, newValue) -> {
devModeStep = 0;
@StyleRes int themeResId;
switch (String.valueOf(newValue)) {
default:
case "system":
themeResId = R.style.Theme_MagiskModuleManager;
break;
case "dark":
themeResId = R.style.Theme_MagiskModuleManager_Dark;
break;
case "light":
themeResId = R.style.Theme_MagiskModuleManager_Light;
break;
}
MainApplication.getINSTANCE().setManagerThemeResId(themeResId);
CompatActivity.getCompatActivity(this).setThemeRecreate(themeResId);
UiThreadHandler.handler.postDelayed(() -> {
MainApplication.getINSTANCE().updateTheme();
CompatActivity.getCompatActivity(this).setThemeRecreate(
MainApplication.getINSTANCE().getManagerThemeResId());
}, 1);
return true;
});
Preference forceEnglish = findPreference("pref_force_english");

@ -0,0 +1,15 @@
package com.fox2code.mmm.utils;
import androidx.annotation.NonNull;
public final class FastException extends RuntimeException {
public static final FastException INSTANCE = new FastException();
private FastException() {}
@NonNull
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}

@ -2,11 +2,14 @@ package com.fox2code.mmm.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.util.Log;
import androidx.annotation.NonNull;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.installer.InstallerInitializer;
import java.io.ByteArrayOutputStream;
import java.io.File;
@ -22,10 +25,12 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import okhttp3.Cache;
import okhttp3.ConnectionSpec;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.Dns;
@ -79,17 +84,30 @@ public class Http {
Log.e(TAG, "Failed to init DoH", e);
}
httpclientBuilder.cookieJar(CookieJar.NO_COOKIES);
httpclientBuilder.addInterceptor(chain -> {
Request.Builder request = chain.request().newBuilder();
if (InstallerInitializer.peekMagiskPath() != null) {
request.header("User-Agent", // Declare Magisk version to the server
"Magisk/" + InstallerInitializer.peekMagiskVersion());
}
if (chain.request().header("Accept-Language") == null) {
request.header("Accept-Language", // Send system language to the server
Resources.getSystem().getConfiguration().locale.toLanguageTag());
}
return chain.proceed(request.build());
});
MainApplication mainApplication = MainApplication.getINSTANCE();
if (mainApplication != null) {
// Fallback DNS cache responses in case request fail but already succeeded once in the past
httpclientBuilder.dns(fallbackDNS = new FallBackDNS(mainApplication, dns,
"github.com", "api.github.com", "raw.githubusercontent.com",
"camo.githubusercontent.com", "user-images.githubusercontent.com",
"cdn.jsdelivr.net", "img.shields.io", "magisk-modules-repo.github.io",
"www.androidacy.com"));
"www.androidacy.com", "api.androidacy.com"));
httpClient = httpclientBuilder.build();
httpclientBuilder.cache(new Cache(
new File(mainApplication.getCacheDir(), "http_cache"),
2L * 1024L * 1024L)); // 2Mib of cache
16L * 1024L * 1024L)); // 16Mib of cache
httpclientBuilder.cookieJar(new CDNCookieJar());
httpClientWithCache = httpclientBuilder.build();
Log.i(TAG, "Initialized Http successfully!");
@ -316,4 +334,34 @@ public class Http {
return inetAddresses;
}
}
/**
* Change URL to appropriate url and force Magisk link to use latest version.
*/
public static String updateLink(String string) {
if (string.startsWith("https://cdn.jsdelivr.net/gh/Magisk-Modules-Repo/")) {
int start = string.lastIndexOf('@'),
end = string.lastIndexOf('/');
if ((end - 8) <= start) return string; // Skip if not a commit id
return string.substring(0, start + 1) + "master" + string.substring(end);
}
if (string.startsWith("https://github.com/Magisk-Modules-Repo/")) {
int i = string.lastIndexOf("/archive/");
if (i != -1) return string.substring(0, i + 9) + "master.zip";
}
return fixUpLink(string);
}
/**
* Change URL to appropriate url
*/
public static String fixUpLink(String string) {
if (string.startsWith("https://raw.githubusercontent.com/")) {
String[] tokens = string.substring(34).split("/", 4);
if (tokens.length != 4) return string;
return "https://cdn.jsdelivr.net/gh/" +
tokens[0] + "/" + tokens[1] + "@" + tokens[2] + "/" + tokens[3];
}
return string;
}
}

@ -8,15 +8,22 @@ import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
public class PropUtils {
private static final HashMap<String, String> moduleSupportsFallbacks = new HashMap<>();
private static final HashMap<String, String> moduleConfigsFallbacks = new HashMap<>();
private static final HashMap<String, Integer> moduleMinApiFallbacks = new HashMap<>();
private static final HashSet<String> moduleImportantProp = new HashSet<>(Arrays.asList(
"id", "name", "version", "versionCode"
));
private static final int RIRU_MIN_API;
// Note: These fallback values may not be up-to-date
@ -50,11 +57,23 @@ public class PropUtils {
moduleMinApiFallbacks.put("riru-core", RIRU_MIN_API = Build.VERSION_CODES.M);
}
public static void readProperties(ModuleInfo moduleInfo, String file,boolean local) throws IOException {
public static void readProperties(ModuleInfo moduleInfo, String file,
boolean local) throws IOException {
readProperties(moduleInfo, SuFileInputStream.open(file), file, local);
}
public static void readProperties(ModuleInfo moduleInfo, String file,
String name, boolean local) throws IOException {
readProperties(moduleInfo, SuFileInputStream.open(file), name, local);
}
public static void readProperties(ModuleInfo moduleInfo, InputStream inputStream,
String name, boolean local) throws IOException {
boolean readId = false, readIdSec = false, readName = false,
readVersionCode = false, readVersion = false, invalid = false;
readVersionCode = false, readVersion = false, readDescription = false,
readUpdateJson = false, invalid = false;
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(SuFileInputStream.open(file), StandardCharsets.UTF_8))) {
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
int lineNum = 0;
while ((line = bufferedReader.readLine()) != null) {
@ -70,11 +89,15 @@ public class PropUtils {
invalid = true;
continue;
} else throw new IOException("Invalid key at line " + lineNum);
} else if (isInvalidValue(value) && !key.equals("id") && !key.equals("name")) {
if (local) {
invalid = true;
continue;
} else throw new IOException("Invalid value for key " + key);
} else {
if (value.isEmpty() && !moduleImportantProp.contains(key))
continue; // allow empty values to pass.
if (isInvalidValue(value)) {
if (local) {
invalid = true;
continue;
} else throw new IOException("Invalid value for key " + key);
}
}
switch (key) {
case "id":
@ -89,7 +112,7 @@ public class PropUtils {
if (local) {
invalid = true;
} else {
throw new IOException(file + " has an non matching module id! " +
throw new IOException(name + " has an non matching module id! " +
"(Expected \"" + moduleInfo.id + "\" got \"" + value + "\"");
}
}
@ -134,6 +157,12 @@ public class PropUtils {
break;
case "description":
moduleInfo.description = value;
readDescription = true;
break;
case "updateJson":
if (isInvalidURL(value)) break;
moduleInfo.updateJson = Http.fixUpLink(value);
readUpdateJson = true;
break;
case "support":
// Do not accept invalid or too broad support links
@ -198,13 +227,19 @@ public class PropUtils {
throw new IOException("Didn't read module versionCode at least once!");
}
}
if (moduleInfo.name == null || !readName) {
if (!readName || isInvalidValue(moduleInfo.name)) {
moduleInfo.name = makeNameFromId(moduleInfo.id);
}
// We can't accept too long version names for usability reason.
if (moduleInfo.version == null || !readVersion || moduleInfo.version.length() > 16) {
if (!readVersion || moduleInfo.version.length() > 16) {
moduleInfo.version = "v" + moduleInfo.versionCode;
}
if (!readDescription || isInvalidValue(moduleInfo.description)) {
moduleInfo.description = "";
}
if (!readUpdateJson) {
moduleInfo.updateJson = null;
}
if (moduleInfo.minApi == 0) {
Integer minApiFallback = moduleMinApiFallbacks.get(moduleInfo.id);
if (minApiFallback != null)

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M14.94,4.66h-4.72l2.36,-2.36zM10.25,19.37h4.66l-2.33,2.33zM6.1,6.27L1.6,17.73h1.84l0.92,-2.45h5.11l0.92,2.45h1.84L7.74,6.27L6.1,6.27zM4.97,13.64l1.94,-5.18 1.94,5.18L4.97,13.64zM15.73,16.14h6.12v1.59h-8.53v-1.29l5.92,-8.56h-5.88v-1.6h8.3v1.26l-5.93,8.6z"/>
</vector>

@ -9,10 +9,17 @@
<string name="showcase_mode">The application is in lockdown mode</string>
<string name="failed_download">Failed to download file.</string>
<string name="slow_modules">Modules took too long to boot, consider disabling some modules</string>
<string name="fail_internet">Fail to connect to the internet</string>
<string name="fail_internet">Failed to connect to the internet</string>
<string name="title_activity_settings">SettingsActivity</string>
<string name="app_update_available">Application update available</string>
<string name="app_update">Update</string>
<string name="no_desc_found">No description found.</string>
<string name="download_module">Download module</string>
<string name="install_module">Install module</string>
<string name="update_module">Update module</string>
<string name="changelog">Changelog</string>
<string name="support">Support</string>
<string name="donate">Donate</string>
<!-- Module section translation -->
<string name="module_last_update">Last update:</string>
@ -61,4 +68,17 @@
Some modules do not declare their metadata properly,causing visual glitches,
and/or indicating poor module quality, disable at your own risk!
</string>
<string name="dns_over_https_pref">Dns over https</string>
<string name="dns_over_https_desc">May fix connections issues in some cases.</string>
<string name="disable_extensions_pref">Disable extensions</string>
<string name="disable_extensions_desc">
Disable Fox\'s Mmm extensions, this prevent modules from using
terminal extensions, useful if a module misuse Fox\'s Mmm extensions.
</string>
<string name="warp_text_pref">Warp text</string>
<string name="warp_text_desc">
Warp text to new line instead of putting all text on the same line
</string>
<string name="repo_enabled">Repo enabled</string>
<string name="repo_disabled">Repo disabled</string>
</resources>

@ -31,7 +31,7 @@
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>

@ -8,14 +8,15 @@
app:title="@string/theme_pref"
app:defaultValue="system"
app:entries="@array/theme_values_names"
app:entryValues="@array/theme_values" />
app:entryValues="@array/theme_values"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:key="pref_force_english"
app:icon="@drawable/ic_baseline_language_24"
app:title="@string/force_english_pref"
app:singleLineTitle="false"/>
app:singleLineTitle="false" />
<SwitchPreferenceCompat
app:defaultValue="false"
@ -30,7 +31,8 @@
app:key="pref_showcase_mode"
app:icon="@drawable/ic_baseline_lock_24"
app:title="@string/showcase_mode_pref"
app:summary="@string/showcase_mode_desc"/>
app:summary="@string/showcase_mode_desc"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
app:defaultValue="false"
@ -63,26 +65,31 @@
app:key="pref_repo_main"
app:icon="@drawable/ic_baseline_extension_24"
app:summary="@string/repo_main_desc"
app:title="@string/loading" />
app:title="@string/loading"
app:singleLineTitle="false" />
<Preference
app:key="pref_repo_alt"
app:icon="@drawable/ic_baseline_extension_24"
app:summary="@string/repo_main_alt"
app:title="@string/loading" />
app:title="@string/loading"
app:singleLineTitle="false" />
</PreferenceCategory>
<PreferenceCategory
app:title="@string/pref_category_info">
<Preference
app:key="pref_update"
app:icon="@drawable/ic_baseline_system_update_24"
app:title="@string/app_update" />
app:title="@string/app_update"
app:singleLineTitle="false" />
<Preference
app:key="pref_source_code"
app:icon="@drawable/ic_github"
app:title="@string/source_code" />
app:title="@string/source_code"
app:singleLineTitle="false" />
<Preference
app:key="pref_show_licenses"
app:icon="@drawable/ic_baseline_info_24"
app:title="@string/show_licenses" />
app:title="@string/show_licenses"
app:singleLineTitle="false" />
</PreferenceCategory>
</PreferenceScreen>

@ -7,7 +7,7 @@ buildscript {
}
project.ext.latestAboutLibsRelease = "8.9.4"
dependencies {
classpath 'com.android.tools.build:gradle:7.0.4'
classpath 'com.android.tools.build:gradle:7.1.0-rc01'
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${latestAboutLibsRelease}"
// NOTE: Do not place your application dependencies here; they belong

Loading…
Cancel
Save