0.2.5 Release

pull/15/head 0.2.5
Fox2Code 2 years ago
parent 754e3890cf
commit 5f6e92174d

@ -12,7 +12,7 @@ Index:
## Properties
In addition to the following magisk properties
In addition to the following required magisk properties
```properties
# Magisk supported properties
id=<string>
@ -22,8 +22,9 @@ versionCode=<int>
author=<string>
description=<string>
```
(Note: The Fox's mmm will not show the module if theses values are not filled properly)
This the manager support these new properties
This the manager support these new optional properties
```properties
# Fox's Mmm supported properties
minApi=<int>
@ -38,7 +39,7 @@ config=<package>
- `minApi` and `maxApi` tell the manager which is the SDK version range the module support
(See: [Codenames, Tags, and Build Numbers](https://source.android.com/setup/start/build-numbers))
- `minMagisk` tell the manager which is the minimum Magisk version required for the module
(Often for magisk `xx.y` the version code is `xxy00`)
(Often for magisk `xx.y` the version code is `xxyzz`, `zz` being non `00` on canary builds)
- `support` support link to direct users when they need support for you modules
- `donate` donate link to direct users to where they can financially support your project
- `config` package name of the application that configure your module

@ -10,10 +10,14 @@ So I made my own app to do that! :3
Minimum:
- Android 5.0+
- Magisk 19.0+
- An internet connection
Recommended:
- Android 6.0+
- Magisk 21.2+
- An internet connection
Note: This app may require the use of a VPN in countries with a state wide firewall.
## For users

@ -10,8 +10,8 @@ android {
applicationId "com.fox2code.mmm"
minSdk 21
targetSdk 31
versionCode 14
versionName "0.2.4"
versionCode 15
versionName "0.2.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

@ -4,6 +4,9 @@
package="com.fox2code.mmm"
tools:ignore="QueryAllPackagesPermission">
<!-- Wifi is not the only way to get an internet connection -->
<uses-feature android:name="android.hardware.wifi" android:required="false" />
<!-- Retrieve online modules -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Make sure of the module active state by checking enabled modules on boot -->

@ -37,13 +37,13 @@ public class AppUpdateManager {
// Return true if should show a notification
public boolean checkUpdate(boolean force) {
if (this.peekShouldUpdate())
if (!force && this.peekShouldUpdate())
return true;
long lastChecked = this.lastChecked;
if (!force && lastChecked != 0 &&
if (lastChecked != 0 &&
// Avoid spam calls by putting a 10 seconds timer
lastChecked < System.currentTimeMillis() - 10000L)
return false;
return force && this.peekShouldUpdate();
synchronized (this.updateLock) {
if (lastChecked != this.lastChecked)
return this.peekShouldUpdate();

@ -21,6 +21,7 @@ import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.settings.SettingsActivity;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.google.android.material.progressindicator.LinearProgressIndicator;
@ -180,7 +181,7 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O
moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE);
if (!RepoManager.getINSTANCE().hasConnectivity())
moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
else if (AppUpdateManager.getAppUpdateManager().checkUpdate(true))
else if (AppUpdateManager.getAppUpdateManager().checkUpdate(false))
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
moduleViewListBuilder.appendRemoteModules();
Log.i(TAG, "Common Before applyTo");
@ -201,8 +202,14 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O
this.progressIndicator.setProgressCompat(0, false);
// this.swipeRefreshLayout.setRefreshing(true); ??
new Thread(() -> {
Http.cleanDnsCache(); // Allow DNS reload from network
RepoManager.getINSTANCE().update(value -> runOnUiThread(() ->
this.progressIndicator.setProgressCompat((int) (value * PRECISION), true)));
this.progressIndicator.setProgressCompat(
(int) (value * PRECISION), true)));
if (!RepoManager.getINSTANCE().hasConnectivity())
moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET);
else if (AppUpdateManager.getAppUpdateManager().checkUpdate(true))
moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE);
runOnUiThread(() -> {
this.progressIndicator.setVisibility(View.GONE);
this.swipeRefreshLayout.setRefreshing(false);

@ -261,6 +261,9 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter<ModuleViewAdap
int backgroundAttr = R.attr.colorBackgroundFloating;
if (type == ModuleHolder.Type.NOTIFICATION) {
backgroundAttr = moduleHolder.notificationType.backgroundAttr;
} else if (type == ModuleHolder.Type.INSTALLED &&
moduleHolder.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
backgroundAttr = R.attr.colorError;
}
Resources.Theme theme = this.cardView.getContext().getTheme();
TypedValue value = new TypedValue();

@ -112,8 +112,8 @@ public class ModuleViewListBuilder {
}
}
newNotificationsLen = this.notifications.size() - special;
EnumSet<ModuleHolder.Type> headerTypes = EnumSet.of(
ModuleHolder.Type.NOTIFICATION, ModuleHolder.Type.SEPARATOR);
EnumSet<ModuleHolder.Type> headerTypes = EnumSet.of(ModuleHolder.Type.SEPARATOR,
ModuleHolder.Type.NOTIFICATION, ModuleHolder.Type.FOOTER);
Iterator<ModuleHolder> moduleHolderIterator = this.mappedModuleHolders.values().iterator();
synchronized (this.queryLock) {
while (moduleHolderIterator.hasNext()) {

@ -21,10 +21,9 @@ public final class ModuleManager {
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, ModuleInfo> invalidModules;
private final SharedPreferences bootPrefs;
private final Object scanLock = new Object();
private boolean scanning, lastScanResult;
private boolean scanning;
private static final ModuleManager INSTANCE = new ModuleManager();
@ -34,19 +33,17 @@ public final class ModuleManager {
private ModuleManager() {
this.moduleInfos = new HashMap<>();
this.invalidModules = new HashMap<>();
this.bootPrefs = MainApplication.getBootSharedPreferences();
}
// MultiThread friendly method
public final boolean scan() {
public final void scan() {
if (!this.scanning) {
// Do scan
synchronized (scanLock) {
this.scanning = true;
try {
this.lastScanResult =
this.scanInternal();
this.scanInternal();
} finally {
this.scanning = false;
}
@ -55,7 +52,6 @@ public final class ModuleManager {
// Wait for current scan
synchronized (scanLock) {}
}
return this.lastScanResult;
}
// Pause execution until the scan is completed if one is currently running
@ -69,13 +65,9 @@ public final class ModuleManager {
}
}
private boolean scanInternal() {
private void scanInternal() {
boolean firstScan = this.bootPrefs.getBoolean("mm_first_scan", true);
boolean changed = false;
SharedPreferences.Editor editor = firstScan ? this.bootPrefs.edit() : null;
// Reset existing ModuleInfo
this.moduleInfos.putAll(this.invalidModules);
this.invalidModules.clear();
for (ModuleInfo v : this.moduleInfos.values()) {
v.flags |= FLAG_MM_UNPROCESSED;
v.flags &= ~FLAGS_RESET_INIT;
@ -94,7 +86,6 @@ public final class ModuleManager {
if (moduleInfo == null) {
moduleInfo = new ModuleInfo(module);
moduleInfos.put(module, moduleInfo);
changed = true;
// Shis should not really happen, but let's handles theses cases anyway
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UPDATING_ONLY;
}
@ -118,7 +109,7 @@ public final class ModuleManager {
}
try {
PropUtils.readProperties(moduleInfo,
"/data/adb/modules/" + module + "/module.prop");
"/data/adb/modules/" + module + "/module.prop", true);
} catch (Exception e) {
Log.d(TAG, "Failed to parse metadata!", e);
moduleInfo.flags |= FLAG_MM_INVALID;
@ -132,13 +123,12 @@ public final class ModuleManager {
if (moduleInfo == null) {
moduleInfo = new ModuleInfo(module);
moduleInfos.put(module, moduleInfo);
changed = true;
}
moduleInfo.flags &= ~FLAGS_RESET_UPDATE;
moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UPDATING;
try {
PropUtils.readProperties(moduleInfo,
"/data/adb/modules_update/" + module + "/module.prop");
"/data/adb/modules_update/" + module + "/module.prop", true);
} catch (Exception e) {
Log.d(TAG, "Failed to parse metadata!", e);
moduleInfo.flags |= FLAG_MM_INVALID;
@ -152,10 +142,6 @@ public final class ModuleManager {
if ((moduleInfo.flags & FLAG_MM_UNPROCESSED) != 0) {
moduleInfoIterator.remove();
continue; // Don't process fallbacks if unreferenced
} else if ((moduleInfo.flags & FLAG_MM_INVALID) != 0) {
moduleInfo.flags &=~ FLAG_MM_INVALID;
this.invalidModules.put(moduleInfo.id, moduleInfo);
moduleInfoIterator.remove();
}
if (moduleInfo.name == null || (moduleInfo.name.equals(moduleInfo.id))) {
moduleInfo.name = Character.toUpperCase(moduleInfo.id.charAt(0)) +
@ -169,7 +155,6 @@ public final class ModuleManager {
editor.putBoolean("mm_first_scan", false);
editor.apply();
}
return changed;
}
public HashMap<String, ModuleInfo> getModules() {
@ -177,11 +162,6 @@ public final class ModuleManager {
return this.moduleInfos;
}
public HashMap<String, ModuleInfo> getInvalidModules() {
this.afterScan();
return invalidModules;
}
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");

@ -112,7 +112,7 @@ public class RepoData {
if (file.exists()) {
try {
ModuleInfo moduleInfo = repoModule.moduleInfo;
PropUtils.readProperties(moduleInfo, file.getAbsolutePath());
PropUtils.readProperties(moduleInfo, file.getAbsolutePath(), false);
moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID;
if (moduleInfo.version == null) {
moduleInfo.version = "v" + moduleInfo.versionCode;

@ -41,6 +41,7 @@ public class Http {
private static final String TAG = "Http";
private static final OkHttpClient httpClient;
private static final OkHttpClient httpClientWithCache;
private static final FallBackDNS fallbackDNS;
static {
OkHttpClient.Builder httpclientBuilder = new OkHttpClient.Builder();
@ -80,10 +81,11 @@ public class Http {
httpclientBuilder.cookieJar(CookieJar.NO_COOKIES);
MainApplication mainApplication = MainApplication.getINSTANCE();
if (mainApplication != null) {
httpclientBuilder.dns(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"));
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"));
httpClient = httpclientBuilder.build();
httpclientBuilder.cache(new Cache(
new File(mainApplication.getCacheDir(), "http_cache"),
@ -92,6 +94,7 @@ public class Http {
httpClientWithCache = httpclientBuilder.build();
Log.i(TAG, "Initialized Http successfully!");
} else {
fallbackDNS = null;
httpclientBuilder.dns(dns);
httpClientWithCache = httpClient = httpclientBuilder.build();
Log.e(TAG, "Initialized Http too soon!");
@ -166,6 +169,12 @@ public class Http {
return byteArrayOutputStream.toByteArray();
}
public static void cleanDnsCache() {
if (Http.fallbackDNS != null) {
Http.fallbackDNS.cleanDnsCache();
}
}
/**
* Cookie jar that allow CDN cookies, reset on app relaunch
* Note: An argument can be made that it allow tracking but
@ -244,36 +253,44 @@ public class Http {
@Override
public List<InetAddress> lookup(@NonNull String s) throws UnknownHostException {
if (this.fallbacks.contains(s)) {
List<InetAddress> addresses = this.fallbackCache.get(s);
if (addresses != null)
return addresses;
try {
addresses = this.parent.lookup(s);
if (addresses.isEmpty() || addresses.get(0).isLoopbackAddress())
throw new UnknownHostException(s);
this.fallbackCache.put(s, addresses);
this.sharedPreferences.edit().putString(
s.replace('.', '_'), toString(addresses)).apply();
} catch (UnknownHostException e) {
String key = this.sharedPreferences.getString(
s.replace('.', '_'), "");
if (!key.isEmpty()) try {
addresses = fromString(key);
this.fallbackCache.put(s, addresses);
List<InetAddress> addresses;
synchronized (this.fallbackCache) {
addresses = this.fallbackCache.get(s);
if (addresses != null)
return addresses;
} catch (UnknownHostException e2) {
this.sharedPreferences.edit().remove(
s.replace('.', '_')).apply();
try {
addresses = this.parent.lookup(s);
if (addresses.isEmpty() || addresses.get(0).isLoopbackAddress())
throw new UnknownHostException(s);
this.fallbackCache.put(s, addresses);
this.sharedPreferences.edit().putString(
s.replace('.', '_'), toString(addresses)).apply();
} catch (UnknownHostException e) {
String key = this.sharedPreferences.getString(
s.replace('.', '_'), "");
if (key.isEmpty()) throw e;
try {
addresses = fromString(key);
this.fallbackCache.put(s, addresses);
} catch (UnknownHostException e2) {
this.sharedPreferences.edit().remove(
s.replace('.', '_')).apply();
throw e;
}
}
throw e;
}
return addresses;
} else {
return this.parent.lookup(s);
}
}
void cleanDnsCache() {
synchronized (this.fallbackCache) {
this.fallbackCache.clear();
}
}
@NonNull
private static String toString(@NonNull List<InetAddress> inetAddresses) {
if (inetAddresses.isEmpty()) return "";

@ -38,7 +38,7 @@ public class IntentHelper {
context.startActivity(myIntent);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, "No application can handle this request."
+ " Please install a webbrowser", Toast.LENGTH_SHORT).show();
+ " Please install a web-browser", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}

@ -50,39 +50,83 @@ public class PropUtils {
moduleMinApiFallbacks.put("riru-core", RIRU_MIN_API = Build.VERSION_CODES.M);
}
public static void readProperties(ModuleInfo moduleInfo, String file) throws IOException {
boolean readId = false, readIdSec = false, readVersionCode = false;
public static void readProperties(ModuleInfo moduleInfo, String file,boolean local) throws IOException {
boolean readId = false, readIdSec = false, readName = false,
readVersionCode = false, readVersion = false, invalid = false;
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(SuFileInputStream.open(file), StandardCharsets.UTF_8))) {
String line;
int lineNum = 0;
while ((line = bufferedReader.readLine()) != null) {
lineNum++;
int index = line.indexOf('=');
if (index == -1 || line.startsWith("#"))
continue;
String key = line.substring(0, index);
String value = line.substring(index + 1).trim();
// name and id have their own implementation
if (isInvalidValue(key)) {
if (local) {
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);
}
switch (key) {
case "id":
if (isInvalidValue(value)) {
if (local) {
invalid = true;
break;
} throw new IOException("Invalid module id!");
}
readId = true;
if (!moduleInfo.id.equals(value)) {
throw new IOException(file + " has an non matching module id! "+
"(Expected \"" + moduleInfo.id + "\" got \"" + value + "\"");
if (local) {
invalid = true;
} else {
throw new IOException(file + " has an non matching module id! " +
"(Expected \"" + moduleInfo.id + "\" got \"" + value + "\"");
}
}
break;
case "name":
if (readIdSec && !moduleInfo.id.equals(value))
throw new IOException("Duplicate module name!");
if (readName) {
if (local) {
invalid = true;
break;
} else throw new IOException("Duplicate module name!");
}
if (isInvalidValue(value)) {
if (local) {
invalid = true;
break;
} throw new IOException("Invalid module name!");
}
readName = true;
moduleInfo.name = value;
if (moduleInfo.id.equals(value)) {
readIdSec = true;
}
break;
case "version":
readVersion = true;
moduleInfo.version = value;
break;
case "versionCode":
readVersionCode = true;
moduleInfo.versionCode = Long.parseLong(value);
try {
moduleInfo.versionCode = Long.parseLong(value);
} catch (RuntimeException e) {
if (local) {
invalid = true;
moduleInfo.versionCode = 0;
} else throw e;
}
break;
case "author":
moduleInfo.author = value;
@ -92,14 +136,14 @@ public class PropUtils {
break;
case "support":
// Do not accept invalid or too broad support links
if (!value.startsWith("https://") ||
if (isInvalidURL(value) ||
"https://forum.xda-developers.com/".equals(value))
break;
moduleInfo.support = value;
break;
case "donate":
// Do not accept invalid donate links
if (!value.startsWith("https://")) break;
if (isInvalidURL(value)) break;
moduleInfo.donate = value;
break;
case "config":
@ -136,21 +180,27 @@ public class PropUtils {
}
}
if (!readId) {
if (readIdSec) {
if (readIdSec && local) {
// Using the name for module id is not really appropriate, so beautify it a bit
moduleInfo.name = moduleInfo.id.substring(0, 1).toUpperCase(Locale.ROOT) +
moduleInfo.id.substring(1).replace('_', ' ');
moduleInfo.name = makeNameFromId(moduleInfo.id);
} else if (local) { // Allow local module to not declare ids
invalid = true;
} else {
throw new IOException("Didn't read module id at least once!");
}
}
if (!readVersionCode) {
throw new IOException("Didn't read module versionCode at least once!");
if (local) {
invalid = true;
moduleInfo.versionCode = 0;
} else {
throw new IOException("Didn't read module versionCode at least once!");
}
}
if (moduleInfo.name == null) {
moduleInfo.name = moduleInfo.id;
if (moduleInfo.name == null || !readName) {
moduleInfo.name = makeNameFromId(moduleInfo.id);
}
if (moduleInfo.version == null) {
if (moduleInfo.version == null || !readVersion) {
moduleInfo.version = "v" + moduleInfo.versionCode;
}
if (moduleInfo.minApi == 0) {
@ -167,6 +217,16 @@ public class PropUtils {
if (moduleInfo.config == null) {
moduleInfo.config = moduleConfigsFallbacks.get(moduleInfo.id);
}
// All local modules should have an author
// set to "Unknown" if author is missing.
if (local && moduleInfo.author == null) {
moduleInfo.author = "Unknown";
}
if (invalid) {
moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID;
// This shouldn't happen but just in case
if (!local) throw new IOException("Invalid properties!");
}
}
// Some module are really so low quality that it has become very annoying.
@ -179,4 +239,20 @@ public class PropUtils {
|| description.toLowerCase(Locale.ROOT).equals(moduleInfo.name.toLowerCase(Locale.ROOT))
|| description.length() < Math.min(Math.max(moduleInfo.name.length() + 4, 16), 24);
}
private static boolean isInvalidValue(String name) {
return !TextUtils.isGraphic(name) || name.indexOf('\0') != -1;
}
private static boolean isInvalidURL(String url) {
int i = url.indexOf('/', 8);
int e = url.indexOf('.', 8);
return i == -1 || e == -1 || e >= i || !url.startsWith("https://")
|| url.length() <= 12 || url.indexOf('\0') != -1;
}
private static String makeNameFromId(String moduleId) {
return moduleId.substring(0, 1).toUpperCase(Locale.ROOT) +
moduleId.substring(1).replace('_', ' ');
}
}

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="theme_values">
<item>system</item>
<item>dark</item>
<item>light</item>
</string-array>
<string-array name="theme_values_names">
<item>Systemvorgabe</item>
<item>Dunkel</item>

Loading…
Cancel
Save