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 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 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 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 - `clearTerminal`: Clear the terminal of any text, making it empty
- `scrollUp`: Scroll up at the top of the terminal - `scrollUp`: Scroll up at the top of the terminal
- `scrollDown`: Scroll down at the bottom of the terminal - `scrollDown`: Scroll down at the bottom of the terminal
- `showLoading`: Show an indeterminate progress bar - `showLoading <max>`: Show an indeterminate progress bar
(Note: the bar is automatically hidden when the install finish) (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 - `hideLoading`: Hide the indeterminate progress bar if previously shown
- `setSupportLink <url>`: Set support link to show when the install finish - `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) (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: Note:
The current behavior with unknown command is to ignore them, 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 A wrapper script to use theses commands could be
```sh ```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) [https://github.com/Magisk-Modules-Repo](https://github.com/Magisk-Modules-Repo)
- No longer accept new modules or update to existing modules - 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 - May be shut down at any moment
- Official app dropped support for it - Official app dropped support for it
- Officially supported by Fox's mmm - Officially supported by Fox's mmm

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

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

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

@ -41,8 +41,8 @@ public class AppUpdateManager {
return true; return true;
long lastChecked = this.lastChecked; long lastChecked = this.lastChecked;
if (lastChecked != 0 && if (lastChecked != 0 &&
// Avoid spam calls by putting a 10 seconds timer // Avoid spam calls by putting a 60 seconds timer
lastChecked < System.currentTimeMillis() - 10000L) lastChecked < System.currentTimeMillis() - 60000L)
return force && this.peekShouldUpdate(); return force && this.peekShouldUpdate();
synchronized (this.updateLock) { synchronized (this.updateLock) {
if (lastChecked != this.lastChecked) 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_PATH = "extra_install_path";
public static final String EXTRA_INSTALL_NAME = "extra_install_name"; 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_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_URL = "extra_markdown_url";
public static final String EXTRA_MARKDOWN_TITLE = "extra_markdown_title"; public static final String EXTRA_MARKDOWN_TITLE = "extra_markdown_title";
public static final String EXTRA_MARKDOWN_CONFIG = "extra_markdown_config"; public static final String EXTRA_MARKDOWN_CONFIG = "extra_markdown_config";

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

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

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

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

@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.manager.ModuleManager; import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoManager; import com.fox2code.mmm.repo.RepoManager;
@ -49,7 +50,7 @@ public class ModuleViewListBuilder {
ModuleManager moduleManager = ModuleManager.getINSTANCE(); ModuleManager moduleManager = ModuleManager.getINSTANCE();
moduleManager.runAfterScan(() -> { moduleManager.runAfterScan(() -> {
Log.i(TAG, "A1: " + moduleManager.getModules().size()); 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); ModuleHolder moduleHolder = this.mappedModuleHolders.get(moduleInfo.id);
if (moduleHolder == null) { if (moduleHolder == null) {
this.mappedModuleHolders.put(moduleInfo.id, 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) { private boolean matchFilter(ModuleHolder moduleHolder) {
if (this.query.isEmpty()) return true; if (this.query.isEmpty()) return true;
ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo();

@ -122,7 +122,7 @@ public enum NotificationType implements NotificationTypeCst {
public final boolean special; public final boolean special;
NotificationType(@StringRes int textId, int iconId) { 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) { 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.Intent;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -117,8 +118,13 @@ public class CompatActivity extends AppCompatActivity {
} }
public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) { 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) { if (compatActionBar != null) {
compatActionBar.setDisplayHomeAsUpEnabled(showHomeAsUp); compatActionBar.setDisplayHomeAsUpEnabled(showHomeAsUp);
} else { } else {
@ -192,7 +198,13 @@ public class CompatActivity extends AppCompatActivity {
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) { 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(); android.app.ActionBar actionBar = this.getActionBar();
if (compatActionBar != null ? (compatActionBar.getDisplayOptions() & if (compatActionBar != null ? (compatActionBar.getDisplayOptions() &
androidx.appcompat.app.ActionBar.DISPLAY_HOME_AS_UP) != 0 : androidx.appcompat.app.ActionBar.DISPLAY_HOME_AS_UP) != 0 :

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

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

@ -1,8 +1,6 @@
package com.fox2code.mmm.installer; package com.fox2code.mmm.installer;
import android.graphics.Color;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; 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 long versionCode;
public String author; public String author;
public String description; public String description;
public String updateJson;
// Community meta // Community meta
public String support; public String support;
public String donate; public String donate;
@ -37,6 +38,23 @@ public class ModuleInfo {
this.name = id; 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) { public boolean hasFlag(int flag) {
return (this.flags & flag) != 0; return (this.flags & flag) != 0;
} }

@ -4,10 +4,19 @@ import android.content.SharedPreferences;
import android.util.Log; import android.util.Log;
import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.PropUtils; import com.fox2code.mmm.utils.PropUtils;
import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile; 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.HashMap;
import java.util.Iterator; import java.util.Iterator;
@ -20,9 +29,10 @@ public final class ModuleManager {
ModuleInfo.FLAG_MODULE_DISABLED | ModuleInfo.FLAG_MODULE_UPDATING | ModuleInfo.FLAG_MODULE_DISABLED | ModuleInfo.FLAG_MODULE_UPDATING |
ModuleInfo.FLAG_MODULE_UNINSTALLING | ModuleInfo.FLAG_MODULE_ACTIVE; ModuleInfo.FLAG_MODULE_UNINSTALLING | ModuleInfo.FLAG_MODULE_ACTIVE;
private static final int FLAGS_RESET_UPDATE = FLAG_MM_INVALID | FLAG_MM_UNPROCESSED; 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 SharedPreferences bootPrefs;
private final Object scanLock = new Object(); private final Object scanLock = new Object();
private int updatableModuleCount = 0;
private boolean scanning; private boolean scanning;
private static final ModuleManager INSTANCE = new ModuleManager(); private static final ModuleManager INSTANCE = new ModuleManager();
@ -75,16 +85,18 @@ public final class ModuleManager {
v.version = null; v.version = null;
v.versionCode = 0; v.versionCode = 0;
v.author = null; v.author = null;
v.description = "No description found."; v.description = "";
v.support = null; v.support = null;
v.config = null; v.config = null;
} }
String[] modules = new SuFile("/data/adb/modules").list(); String[] modules = new SuFile("/data/adb/modules").list();
if (modules != null) { if (modules != null) {
for (String module : modules) { 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) { if (moduleInfo == null) {
moduleInfo = new ModuleInfo(module); moduleInfo = new LocalModuleInfo(module);
moduleInfos.put(module, moduleInfo); moduleInfos.put(module, moduleInfo);
// Shis should not really happen, but let's handles theses cases anyway // Shis should not really happen, but let's handles theses cases anyway
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UPDATING_ONLY; 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(); String[] modules_update = new SuFile("/data/adb/modules_update").list();
if (modules_update != null) { if (modules_update != null) {
for (String module : modules_update) { for (String module : modules_update) {
ModuleInfo moduleInfo = moduleInfos.get(module); LocalModuleInfo moduleInfo = moduleInfos.get(module);
if (moduleInfo == null) { if (moduleInfo == null) {
moduleInfo = new ModuleInfo(module); moduleInfo = new LocalModuleInfo(module);
moduleInfos.put(module, moduleInfo); moduleInfos.put(module, moduleInfo);
} }
moduleInfo.flags &= ~FLAGS_RESET_UPDATE; 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(); this.moduleInfos.values().iterator();
while (moduleInfoIterator.hasNext()) { while (moduleInfoIterator.hasNext()) {
ModuleInfo moduleInfo = moduleInfoIterator.next(); LocalModuleInfo moduleInfo = moduleInfoIterator.next();
if ((moduleInfo.flags & FLAG_MM_UNPROCESSED) != 0) { if ((moduleInfo.flags & FLAG_MM_UNPROCESSED) != 0) {
moduleInfoIterator.remove(); moduleInfoIterator.remove();
continue; // Don't process fallbacks if unreferenced 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))) { if (moduleInfo.name == null || (moduleInfo.name.equals(moduleInfo.id))) {
moduleInfo.name = Character.toUpperCase(moduleInfo.id.charAt(0)) + moduleInfo.name = Character.toUpperCase(moduleInfo.id.charAt(0)) +
moduleInfo.id.substring(1).replace('_', ' '); moduleInfo.id.substring(1).replace('_', ' ');
} }
if (moduleInfo.version == null) { if (moduleInfo.version == null || moduleInfo.version.trim().isEmpty()) {
moduleInfo.version = "v" + moduleInfo.versionCode; 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(); this.afterScan();
return this.moduleInfos; return this.moduleInfos;
} }
public int getUpdatableModuleCount() {
this.afterScan();
return this.updatableModuleCount;
}
public boolean setEnabledState(ModuleInfo moduleInfo, boolean checked) { public boolean setEnabledState(ModuleInfo moduleInfo, boolean checked) {
if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING) && !checked) return false; if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING) && !checked) return false;
SuFile disable = new SuFile("/data/adb/modules/" + moduleInfo.id + "/disable"); SuFile disable = new SuFile("/data/adb/modules/" + moduleInfo.id + "/disable");
@ -200,8 +226,28 @@ public final class ModuleManager {
public boolean masterClear(ModuleInfo moduleInfo) { public boolean masterClear(ModuleInfo moduleInfo) {
if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_ACTIVE)) return false; if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_ACTIVE)) return false;
Shell.su("rm -rf /data/adb/modules/" + moduleInfo.id + "/").exec(); String escapedId = moduleInfo.id.replace("\\", "\\\\")
Shell.su("rm -rf /data/adb/modules_update/" + moduleInfo.id + "/").exec(); .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; moduleInfo.flags = ModuleInfo.FLAG_METADATA_INVALID;
return true; return true;
} }

@ -1,10 +1,12 @@
package com.fox2code.mmm.repo; package com.fox2code.mmm.repo;
import android.annotation.SuppressLint;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.text.TextUtils; import android.util.Log;
import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.utils.Files; import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.PropUtils; import com.fox2code.mmm.utils.PropUtils;
import org.json.JSONArray; import org.json.JSONArray;
@ -12,11 +14,18 @@ import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
public class RepoData { public class RepoData {
private final Object populateLock = new Object(); private final Object populateLock = new Object();
@ -24,30 +33,64 @@ public class RepoData {
public final File cacheRoot; public final File cacheRoot;
public final SharedPreferences cachedPreferences; public final SharedPreferences cachedPreferences;
public final File metaDataCache; public final File metaDataCache;
public final boolean special;
public final HashMap<String, RepoModule> moduleHashMap; public final HashMap<String, RepoModule> moduleHashMap;
public long lastUpdate; public long lastUpdate;
public String name; public String name;
private final Map<String, Long> specialTimes;
private long specialLastUpdate;
RepoData(String url, File cacheRoot, SharedPreferences cachedPreferences) { 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.url = url;
this.cacheRoot = cacheRoot; this.cacheRoot = cacheRoot;
this.cachedPreferences = cachedPreferences; this.cachedPreferences = cachedPreferences;
this.metaDataCache = new File(cacheRoot, "modules.json"); this.metaDataCache = new File(cacheRoot, "modules.json");
this.special = special;
this.moduleHashMap = new HashMap<>(); this.moduleHashMap = new HashMap<>();
this.name = this.url; // Set url as default name this.name = this.url; // Set url as default name
this.specialTimes = special ? new HashMap<>() : Collections.emptyMap();
if (!this.cacheRoot.isDirectory()) { if (!this.cacheRoot.isDirectory()) {
this.cacheRoot.mkdirs(); this.cacheRoot.mkdirs();
} else if (this.metaDataCache.exists()) { } else {
try { if (special) { // Special times need to be loaded before populate
List<RepoModule> modules = this.populate(new JSONObject( File metaDataCacheSpecial = new File(cacheRoot, "modules_times.json");
new String(Files.read(this.metaDataCache), StandardCharsets.UTF_8))); if (metaDataCacheSpecial.exists()) {
for (RepoModule repoModule: modules) { try {
if (!this.tryLoadMetadata(repoModule)) { JSONArray jsonArray = new JSONArray(new String(
repoModule.moduleInfo.flags &=~ ModuleInfo.FLAG_METADATA_INVALID; 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()) { for (RepoModule repoModule : this.moduleHashMap.values()) {
repoModule.processed = false; repoModule.processed = false;
} }
Log.d("RepoData", "Data: " + this.specialTimes.toString());
JSONArray array = jsonObject.getJSONArray("modules"); JSONArray array = jsonObject.getJSONArray("modules");
int len = array.length(); int len = array.length();
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
@ -69,10 +113,23 @@ public class RepoData {
String moduleId = module.getString("id"); String moduleId = module.getString("id");
// Deny remote modules ids shorter than 3 chars long or that start with a digit // 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; if (moduleId.length() < 3 || Character.isDigit(moduleId.charAt(0))) continue;
Long moduleLastUpdateSpecial = this.specialTimes.get(moduleId);
long moduleLastUpdate = module.getLong("last_update"); long moduleLastUpdate = module.getLong("last_update");
String moduleNotesUrl = module.getString("notes_url"); String moduleNotesUrl = module.getString("notes_url");
String modulePropsUrl = module.getString("prop_url"); String modulePropsUrl = module.getString("prop_url");
String moduleZipUrl = module.getString("zip_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); RepoModule repoModule = this.moduleHashMap.get(moduleId);
if (repoModule == null) { if (repoModule == null) {
repoModule = new RepoModule(moduleId); repoModule = new RepoModule(moduleId);
@ -107,12 +164,17 @@ public class RepoData {
return newModules; 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) { public boolean tryLoadMetadata(RepoModule repoModule) {
File file = new File(this.cacheRoot, repoModule.id + ".prop"); File file = new File(this.cacheRoot, repoModule.id + ".prop");
if (file.exists()) { if (file.exists()) {
try { try {
ModuleInfo moduleInfo = repoModule.moduleInfo; 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; moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID;
if (moduleInfo.version == null) { if (moduleInfo.version == null) {
moduleInfo.version = "v" + moduleInfo.versionCode; moduleInfo.version = "v" + moduleInfo.versionCode;
@ -126,6 +188,37 @@ public class RepoData {
return false; 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) { public String getNameOrFallback(String fallback) {
return this.name == null || return this.name == null ||
this.name.equals(this.url) ? this.name.equals(this.url) ?

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

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

@ -59,21 +59,11 @@ public class SettingsActivity extends CompatActivity {
}); });
themePreference.setOnPreferenceChangeListener((preference, newValue) -> { themePreference.setOnPreferenceChangeListener((preference, newValue) -> {
devModeStep = 0; devModeStep = 0;
@StyleRes int themeResId; UiThreadHandler.handler.postDelayed(() -> {
switch (String.valueOf(newValue)) { MainApplication.getINSTANCE().updateTheme();
default: CompatActivity.getCompatActivity(this).setThemeRecreate(
case "system": MainApplication.getINSTANCE().getManagerThemeResId());
themeResId = R.style.Theme_MagiskModuleManager; }, 1);
break;
case "dark":
themeResId = R.style.Theme_MagiskModuleManager_Dark;
break;
case "light":
themeResId = R.style.Theme_MagiskModuleManager_Light;
break;
}
MainApplication.getINSTANCE().setManagerThemeResId(themeResId);
CompatActivity.getCompatActivity(this).setThemeRecreate(themeResId);
return true; return true;
}); });
Preference forceEnglish = findPreference("pref_force_english"); 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.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Resources;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.installer.InstallerInitializer;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
@ -22,10 +25,12 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import okhttp3.Cache; import okhttp3.Cache;
import okhttp3.ConnectionSpec;
import okhttp3.Cookie; import okhttp3.Cookie;
import okhttp3.CookieJar; import okhttp3.CookieJar;
import okhttp3.Dns; import okhttp3.Dns;
@ -79,17 +84,30 @@ public class Http {
Log.e(TAG, "Failed to init DoH", e); Log.e(TAG, "Failed to init DoH", e);
} }
httpclientBuilder.cookieJar(CookieJar.NO_COOKIES); 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(); MainApplication mainApplication = MainApplication.getINSTANCE();
if (mainApplication != null) { 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, httpclientBuilder.dns(fallbackDNS = new FallBackDNS(mainApplication, dns,
"github.com", "api.github.com", "raw.githubusercontent.com", "github.com", "api.github.com", "raw.githubusercontent.com",
"camo.githubusercontent.com", "user-images.githubusercontent.com", "camo.githubusercontent.com", "user-images.githubusercontent.com",
"cdn.jsdelivr.net", "img.shields.io", "magisk-modules-repo.github.io", "cdn.jsdelivr.net", "img.shields.io", "magisk-modules-repo.github.io",
"www.androidacy.com")); "www.androidacy.com", "api.androidacy.com"));
httpClient = httpclientBuilder.build(); httpClient = httpclientBuilder.build();
httpclientBuilder.cache(new Cache( httpclientBuilder.cache(new Cache(
new File(mainApplication.getCacheDir(), "http_cache"), new File(mainApplication.getCacheDir(), "http_cache"),
2L * 1024L * 1024L)); // 2Mib of cache 16L * 1024L * 1024L)); // 16Mib of cache
httpclientBuilder.cookieJar(new CDNCookieJar()); httpclientBuilder.cookieJar(new CDNCookieJar());
httpClientWithCache = httpclientBuilder.build(); httpClientWithCache = httpclientBuilder.build();
Log.i(TAG, "Initialized Http successfully!"); Log.i(TAG, "Initialized Http successfully!");
@ -316,4 +334,34 @@ public class Http {
return inetAddresses; 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.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
public class PropUtils { public class PropUtils {
private static final HashMap<String, String> moduleSupportsFallbacks = new HashMap<>(); private static final HashMap<String, String> moduleSupportsFallbacks = new HashMap<>();
private static final HashMap<String, String> moduleConfigsFallbacks = new HashMap<>(); private static final HashMap<String, String> moduleConfigsFallbacks = new HashMap<>();
private static final HashMap<String, Integer> moduleMinApiFallbacks = 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; private static final int RIRU_MIN_API;
// Note: These fallback values may not be up-to-date // 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); 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, 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( try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(SuFileInputStream.open(file), StandardCharsets.UTF_8))) { new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line; String line;
int lineNum = 0; int lineNum = 0;
while ((line = bufferedReader.readLine()) != null) { while ((line = bufferedReader.readLine()) != null) {
@ -70,11 +89,15 @@ public class PropUtils {
invalid = true; invalid = true;
continue; continue;
} else throw new IOException("Invalid key at line " + lineNum); } else throw new IOException("Invalid key at line " + lineNum);
} else if (isInvalidValue(value) && !key.equals("id") && !key.equals("name")) { } else {
if (local) { if (value.isEmpty() && !moduleImportantProp.contains(key))
invalid = true; continue; // allow empty values to pass.
continue; if (isInvalidValue(value)) {
} else throw new IOException("Invalid value for key " + key); if (local) {
invalid = true;
continue;
} else throw new IOException("Invalid value for key " + key);
}
} }
switch (key) { switch (key) {
case "id": case "id":
@ -89,7 +112,7 @@ public class PropUtils {
if (local) { if (local) {
invalid = true; invalid = true;
} else { } 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 + "\""); "(Expected \"" + moduleInfo.id + "\" got \"" + value + "\"");
} }
} }
@ -134,6 +157,12 @@ public class PropUtils {
break; break;
case "description": case "description":
moduleInfo.description = value; moduleInfo.description = value;
readDescription = true;
break;
case "updateJson":
if (isInvalidURL(value)) break;
moduleInfo.updateJson = Http.fixUpLink(value);
readUpdateJson = true;
break; break;
case "support": case "support":
// Do not accept invalid or too broad support links // 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!"); 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); moduleInfo.name = makeNameFromId(moduleInfo.id);
} }
// We can't accept too long version names for usability reason. // 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; moduleInfo.version = "v" + moduleInfo.versionCode;
} }
if (!readDescription || isInvalidValue(moduleInfo.description)) {
moduleInfo.description = "";
}
if (!readUpdateJson) {
moduleInfo.updateJson = null;
}
if (moduleInfo.minApi == 0) { if (moduleInfo.minApi == 0) {
Integer minApiFallback = moduleMinApiFallbacks.get(moduleInfo.id); Integer minApiFallback = moduleMinApiFallbacks.get(moduleInfo.id);
if (minApiFallback != null) 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="showcase_mode">The application is in lockdown mode</string>
<string name="failed_download">Failed to download file.</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="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="title_activity_settings">SettingsActivity</string>
<string name="app_update_available">Application update available</string> <string name="app_update_available">Application update available</string>
<string name="app_update">Update</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 --> <!-- Module section translation -->
<string name="module_last_update">Last update:</string> <string name="module_last_update">Last update:</string>
@ -61,4 +68,17 @@
Some modules do not declare their metadata properly,causing visual glitches, Some modules do not declare their metadata properly,causing visual glitches,
and/or indicating poor module quality, disable at your own risk! and/or indicating poor module quality, disable at your own risk!
</string> </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> </resources>

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

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

@ -7,7 +7,7 @@ buildscript {
} }
project.ext.latestAboutLibsRelease = "8.9.4" project.ext.latestAboutLibsRelease = "8.9.4"
dependencies { 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}" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${latestAboutLibsRelease}"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

Loading…
Cancel
Save