and the kotlin keeps coming

just keeps on coming

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/27/head
androidacy-user 2 years ago
parent a73a7b613e
commit 7068288d07

@ -202,7 +202,7 @@ class AndroidacyActivity : FoxActivity() {
if (downloadMode || backOnResume) return true
// sanitize url
@Suppress("NAME_SHADOWING") var url = request.url.toString()
url = AndroidacyUtil.hideToken(url).toString()
url = AndroidacyUtil.hideToken(url)
Timber.i("Exiting WebView %s", url)
IntentHelper.openUri(view.context, request.url.toString())
return true

@ -417,7 +417,7 @@ public final class AndroidacyRepoData extends RepoData {
private String injectToken(String url) {
// Do not inject token for non Androidacy urls
if (!AndroidacyUtil.isAndroidacyLink(url)) return url;
if (!AndroidacyUtil.Companion.isAndroidacyLink(url)) return url;
if (this.testMode) {
if (url.startsWith("https://production-api.androidacy.com/")) {
Timber.e("Got non test mode url: %s", AndroidacyUtil.hideToken(url));

@ -1,138 +1,139 @@
package com.fox2code.mmm.androidacy;
@file:Suppress("unused")
import android.net.Uri;
package com.fox2code.mmm.androidacy
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.net.Uri
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet
import java.io.IOException
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.utils.io.net.Http;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;
public enum AndroidacyUtil {
enum class AndroidacyUtil {
;
public static final String REFERRER = "utm_source=FoxMMM&utm_medium=app";
public static boolean isAndroidacyLink(@Nullable Uri uri) {
return uri != null && isAndroidacyLink(uri.toString(), uri);
}
public static boolean isAndroidacyLink(@Nullable String url) {
return url != null && isAndroidacyLink(url, Uri.parse(url));
}
static boolean isAndroidacyLink(@NonNull String url, @NonNull Uri uri) {
int i; // Check both string and Uri to mitigate parse exploit
return url.startsWith("https://") && (i = url.indexOf("/", 8)) != -1 && url.substring(8, i).endsWith("api.androidacy.com") && Objects.requireNonNull(uri.getHost()).endsWith("api.androidacy.com");
}
companion object {
const val REFERRER = "utm_source=FoxMMM&utm_medium=app"
fun isAndroidacyLink(uri: Uri?): Boolean {
return uri != null && isAndroidacyLink(uri.toString(), uri)
}
public static boolean isAndroidacyFileUrl(@Nullable String url) {
if (url == null)
return false;
for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://production-api.androidacy.com/magisk/file/", "https://staging-api.androidacy.com/magisk/file/"}) { // Make both staging and non staging act the same
if (url.startsWith(prefix))
return true;
fun isAndroidacyLink(url: String?): Boolean {
return url != null && isAndroidacyLink(url, Uri.parse(url))
}
return false;
}
// Avoid logging token
public static String hideToken(@NonNull String url) {
// for token, device_id, and client_id, replace with <hidden> by using replaceAll to match until the next non-alphanumeric character or end
// Also, URL decode
url = Uri.decode(url);
url = url + "&";
url = url.replaceAll("token=[^&]*", "token=<hidden>");
url = url.replaceAll("device_id=[^&]*", "device_id=<hidden>");
url = url.replaceAll("client_id=[^&]*", "client_id=<hidden>");
// remove last & added at the end
url = url.substring(0, url.length() - 1);
return url;
}
fun isAndroidacyLink(url: String, uri: Uri): Boolean {
var i = 0// Check both string and Uri to mitigate parse exploit
return url.startsWith("https://") && url.indexOf("/", 8)
.also { i = it } != -1 && url.substring(8, i)
.endsWith("api.androidacy.com") && uri.host?.endsWith("api.androidacy.com") ?: false
}
public static String getModuleId(String moduleUrl) {
// Get the &module= part
int i = moduleUrl.indexOf("&module=");
String moduleId;
// Match until next & or end
if (i != -1) {
int j = moduleUrl.indexOf('&', i + 1);
if (j == -1) {
moduleId = moduleUrl.substring(i + 8);
} else {
moduleId = moduleUrl.substring(i + 8, j);
@JvmStatic
fun isAndroidacyFileUrl(url: String?): Boolean {
if (url == null) return false
for (prefix in arrayOf(
"https://production-api.androidacy.com/downloads/",
"https://production-api.androidacy.com/magisk/file/",
"https://staging-api.androidacy.com/magisk/file/"
)) { // Make both staging and non staging act the same
if (url.startsWith(prefix)) return true
}
// URL decode
moduleId = Uri.decode(moduleId);
// Strip non alphanumeric
moduleId = moduleId.replaceAll("[^a-zA-Z\\d]", "");
return moduleId;
return false
}
if (BuildConfig.DEBUG) {
throw new IllegalArgumentException("Invalid module url: " + moduleUrl);
// Avoid logging token
@Suppress("NAME_SHADOWING")
@JvmStatic
fun hideToken(url: String): String {
// for token, device_id, and client_id, replace with <hidden> by using replaceAll to match until the next non-alphanumeric character or end
// Also, URL decode
var url = url
url = Uri.decode(url)
url = "$url&"
url = url.replace("token=[^&]*".toRegex(), "token=<hidden>")
url = url.replace("device_id=[^&]*".toRegex(), "device_id=<hidden>")
url = url.replace("client_id=[^&]*".toRegex(), "client_id=<hidden>")
// remove last & added at the end
url = url.substring(0, url.length - 1)
return url
}
return null;
}
public static String getModuleTitle(String moduleUrl) {
// Get the &title= part
int i = moduleUrl.indexOf("&moduleTitle=");
// Match until next & or end
if (i != -1) {
int j = moduleUrl.indexOf('&', i + 1);
if (j == -1) {
return Uri.decode(moduleUrl.substring(i + 13));
} else {
return Uri.decode(moduleUrl.substring(i + 13, j));
@JvmStatic
fun getModuleId(moduleUrl: String): String? {
// Get the &module= part
val i = moduleUrl.indexOf("&module=")
var moduleId: String
// Match until next & or end
if (i != -1) {
val j = moduleUrl.indexOf('&', i + 1)
moduleId = if (j == -1) {
moduleUrl.substring(i + 8)
} else {
moduleUrl.substring(i + 8, j)
}
// URL decode
moduleId = Uri.decode(moduleId)
// Strip non alphanumeric
moduleId = moduleId.replace("[^a-zA-Z\\d]".toRegex(), "")
return moduleId
}
require(!BuildConfig.DEBUG) { "Invalid module url: $moduleUrl" }
return null
}
return null;
}
public static String getChecksumFromURL(String moduleUrl) {
// Get the &version= part
int i = moduleUrl.indexOf("&checksum=");
// Match until next & or end
if (i != -1) {
int j = moduleUrl.indexOf('&', i + 1);
if (j == -1) {
return moduleUrl.substring(i + 10);
} else {
return moduleUrl.substring(i + 10, j);
@JvmStatic
fun getModuleTitle(moduleUrl: String): String? {
// Get the &title= part
val i = moduleUrl.indexOf("&moduleTitle=")
// Match until next & or end
if (i != -1) {
val j = moduleUrl.indexOf('&', i + 1)
return if (j == -1) {
Uri.decode(moduleUrl.substring(i + 13))
} else {
Uri.decode(moduleUrl.substring(i + 13, j))
}
}
return null
}
return null;
}
/**
* Check if the url is a premium direct download link
* @param url url to check
* @return true if it is a premium direct download link
* @noinspection unused
*/
public static boolean isPremiumDirectDownloadLink(String url) {
return url.contains("/magisk/ddl/");
}
fun getChecksumFromURL(moduleUrl: String): String? {
// Get the &version= part
val i = moduleUrl.indexOf("&checksum=")
// Match until next & or end
if (i != -1) {
val j = moduleUrl.indexOf('&', i + 1)
return if (j == -1) {
moduleUrl.substring(i + 10)
} else {
moduleUrl.substring(i + 10, j)
}
}
return null
}
/**
* Returns the markdown directly from the API for rendering. Premium only, and internal testing only currently.
* @param url URL to get markdown from
* @return String of markdown
* @noinspection unused
*/
public static String getMarkdownFromAPI(String url) {
byte[] md;
try {
md = Http.doHttpGet(url, false);
} catch (IOException ignored) {
return null;
/**
* Check if the url is a premium direct download link
* @param url url to check
* @return true if it is a premium direct download link
* @noinspection unused
*/
fun isPremiumDirectDownloadLink(url: String): Boolean {
return url.contains("/magisk/ddl/")
}
if (md == null) {
return null;
/**
* Returns the markdown directly from the API for rendering. Premium only, and internal testing only currently.
* @param url URL to get markdown from
* @return String of markdown
* @noinspection unused
*/
fun getMarkdownFromAPI(url: String?): String? {
val md: ByteArray = try {
doHttpGet(url!!, false)
} catch (ignored: IOException) {
return null
}
return String(md)
}
return new String(md);
}
}
}

@ -1,191 +1,212 @@
package com.fox2code.mmm.androidacy;
import android.annotation.SuppressLint;
import android.content.res.Resources;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.util.TypedValue;
import android.webkit.JavascriptInterface;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.Keep;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import com.fox2code.foxcompat.view.FoxDisplay;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.installer.InstallerInitializer;
import com.fox2code.mmm.manager.LocalModuleInfo;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.manager.ModuleManager;
import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.ExternalHelper;
import com.fox2code.mmm.utils.IntentHelper;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.Hashes;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import timber.log.Timber;
@SuppressWarnings({"unused", "SameReturnValue"})
@Keep
public class AndroidacyWebAPI {
public static final int COMPAT_UNSUPPORTED = 0;
public static final int COMPAT_DOWNLOAD = 1;
private static final int MAX_COMPAT_MODE = 1;
private final AndroidacyActivity activity;
private final boolean allowInstall;
boolean consumedAction;
boolean downloadMode;
int effectiveCompatMode;
int notifiedCompatMode;
public AndroidacyWebAPI(AndroidacyActivity activity, boolean allowInstall) {
this.activity = activity;
this.allowInstall = allowInstall;
}
@file:Suppress("NAME_SHADOWING", "MemberVisibilityCanBePrivate")
package com.fox2code.mmm.androidacy
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.util.TypedValue
import android.webkit.JavascriptInterface
import android.widget.Toast
import androidx.annotation.Keep
import androidx.core.content.ContextCompat
import com.fox2code.foxcompat.view.FoxDisplay
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.R
import com.fox2code.mmm.androidacy.AndroidacyUtil.Companion.getModuleId
import com.fox2code.mmm.androidacy.AndroidacyUtil.Companion.getModuleTitle
import com.fox2code.mmm.androidacy.AndroidacyUtil.Companion.hideToken
import com.fox2code.mmm.androidacy.AndroidacyUtil.Companion.isAndroidacyFileUrl
import com.fox2code.mmm.androidacy.AndroidacyUtil.Companion.isAndroidacyLink
import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskPath
import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskVersion
import com.fox2code.mmm.manager.ModuleInfo
import com.fox2code.mmm.manager.ModuleManager.Companion.instance
import com.fox2code.mmm.utils.ExternalHelper
import com.fox2code.mmm.utils.IntentHelper.Companion.openCustomTab
import com.fox2code.mmm.utils.IntentHelper.Companion.openInstaller
import com.fox2code.mmm.utils.IntentHelper.Companion.openUrl
import com.fox2code.mmm.utils.io.Files.Companion.readSU
import com.fox2code.mmm.utils.io.Files.Companion.writeSU
import com.fox2code.mmm.utils.io.Hashes.Companion.checkSumFormat
import com.fox2code.mmm.utils.io.Hashes.Companion.checkSumName
import com.fox2code.mmm.utils.io.Hashes.Companion.checkSumValid
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.Objects
void forceQuitRaw(String error) {
Toast.makeText(this.activity, error, Toast.LENGTH_LONG).show();
this.activity.runOnUiThread(this.activity::forceBackPressed);
this.activity.backOnResume = true; // Set backOnResume just in case
this.downloadMode = false;
}
@Keep
class AndroidacyWebAPI(
private val activity: AndroidacyActivity,
private val allowInstall: Boolean
) {
var consumedAction = false
var downloadMode = false
void openNativeModuleDialogRaw(String moduleUrl, String moduleId, String installTitle, String checksum, boolean canInstall) {
if (BuildConfig.DEBUG)
Timber.d("ModuleDialog, downloadUrl: " + AndroidacyUtil.hideToken(moduleUrl) + ", moduleId: " + moduleId + ", installTitle: " + installTitle + ", checksum: " + checksum + ", canInstall: " + canInstall);
/**
* Allow Androidacy backend to notify compat mode
* return current effective compat mode
*/
@get:JavascriptInterface
var effectiveCompatMode = 0
var notifiedCompatMode = 0
fun forceQuitRaw(error: String?) {
Toast.makeText(activity, error, Toast.LENGTH_LONG).show()
activity.runOnUiThread { activity.forceBackPressed() }
activity.backOnResume = true // Set backOnResume just in case
downloadMode = false
}
fun openNativeModuleDialogRaw(
moduleUrl: String?,
moduleId: String?,
installTitle: String?,
checksum: String?,
canInstall: Boolean
) {
if (BuildConfig.DEBUG) Timber.d(
"ModuleDialog, downloadUrl: " + hideToken(
moduleUrl!!
) + ", moduleId: " + moduleId + ", installTitle: " + installTitle + ", checksum: " + checksum + ", canInstall: " + canInstall
)
// moduleUrl should be a valid URL, i.e. in the androidacy.com domain
// if it is not, do not proceed
if (!AndroidacyUtil.isAndroidacyFileUrl(moduleUrl)) {
Timber.e("ModuleDialog, invalid URL: %s", moduleUrl);
return;
if (!isAndroidacyFileUrl(moduleUrl)) {
Timber.e("ModuleDialog, invalid URL: %s", moduleUrl)
return
}
this.downloadMode = false;
RepoModule repoModule = AndroidacyRepoData.getInstance().moduleHashMap.get(installTitle);
String title, description;
boolean mmtReborn = false;
downloadMode = false
val repoModule = AndroidacyRepoData.getInstance().moduleHashMap[installTitle]
val title: String?
var description: String?
var mmtReborn = false
if (repoModule != null) {
title = repoModule.moduleInfo.name;
description = repoModule.moduleInfo.description;
mmtReborn = repoModule.moduleInfo.mmtReborn;
if (description == null || description.length() == 0) {
description = this.activity.getString(R.string.no_desc_found);
title = repoModule.moduleInfo.name
description = repoModule.moduleInfo.description
mmtReborn = repoModule.moduleInfo.mmtReborn
if (description.isNullOrEmpty()) {
description = activity.getString(R.string.no_desc_found)
}
} else {
// URL Decode installTitle
title = installTitle;
String checkSumType = Hashes.checkSumName(checksum);
if (checkSumType == null) {
description = "Checksum: " + ((checksum == null || checksum.isEmpty()) ? "null" : checksum);
title = installTitle
val checkSumType = checkSumName(checksum)
description = if (checkSumType == null) {
"Checksum: ${if (checksum.isNullOrEmpty()) "null" else checksum}"
} else {
description = checkSumType + ": " + checksum;
"$checkSumType: $checksum"
}
}
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this.activity);
builder.setTitle(title).setMessage(description).setCancelable(true).setIcon(R.drawable.ic_baseline_extension_24);
builder.setNegativeButton(R.string.download_module, (x, y) -> {
this.downloadMode = true;
IntentHelper.openCustomTab(this.activity, moduleUrl);
});
val builder = MaterialAlertDialogBuilder(activity)
builder.setTitle(title).setMessage(description).setCancelable(true)
.setIcon(R.drawable.ic_baseline_extension_24)
builder.setNegativeButton(R.string.download_module) { _: DialogInterface?, _: Int ->
downloadMode = true
openCustomTab(activity, moduleUrl)
}
if (canInstall) {
boolean hasUpdate = false;
String config = null;
var hasUpdate = false
var config: String? = null
if (repoModule != null) {
config = repoModule.moduleInfo.config;
LocalModuleInfo localModuleInfo = Objects.requireNonNull(ModuleManager.getInstance()).getModules().get(repoModule.id);
hasUpdate = localModuleInfo != null && repoModule.moduleInfo.versionCode > localModuleInfo.versionCode;
config = repoModule.moduleInfo.config
val localModuleInfo = instance?.modules?.get(repoModule.id)
hasUpdate =
localModuleInfo != null && repoModule.moduleInfo.versionCode > localModuleInfo.versionCode
}
final String fModuleUrl = moduleUrl, fTitle = title, fConfig = config, fChecksum = checksum;
final boolean fMMTReborn = mmtReborn;
builder.setPositiveButton(hasUpdate ? R.string.update_module : R.string.install_module, (x, y) -> {
IntentHelper.openInstaller(this.activity, fModuleUrl, fTitle, fConfig, fChecksum, fMMTReborn);
val fConfig = config
val fMMTReborn = mmtReborn
builder.setPositiveButton(if (hasUpdate) R.string.update_module else R.string.install_module) { _: DialogInterface?, _: Int ->
openInstaller(
activity,
moduleUrl,
title,
fConfig,
checksum,
fMMTReborn
)
// close activity
this.activity.runOnUiThread(this.activity::finishAndRemoveTask);
});
activity.runOnUiThread { activity.finishAndRemoveTask() }
}
}
builder.setOnCancelListener { _: DialogInterface? ->
if (!activity.backOnResume) consumedAction = false
}
builder.setOnCancelListener(dialogInterface -> {
if (!this.activity.backOnResume)
this.consumedAction = false;
});
ExternalHelper.INSTANCE.injectButton(builder, () -> {
this.downloadMode = true;
ExternalHelper.INSTANCE.injectButton(builder, {
downloadMode = true
try {
return this.activity.downloadFileAsync(moduleUrl);
} catch (
IOException e) {
Timber.e(e, "Failed to download module");
AndroidacyWebAPI.this.activity.runOnUiThread(() -> Toast.makeText(AndroidacyWebAPI.this.activity, R.string.failed_download, Toast.LENGTH_SHORT).show());
return null;
return@injectButton activity.downloadFileAsync(moduleUrl)
} catch (e: IOException) {
Timber.e(e, "Failed to download module")
activity.runOnUiThread {
Toast.makeText(
activity,
R.string.failed_download,
Toast.LENGTH_SHORT
).show()
}
return@injectButton null
}
}, "androidacy_repo");
final int dim5dp = FoxDisplay.dpToPixel(5);
builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp);
this.activity.runOnUiThread(() -> {
AlertDialog alertDialog = builder.show();
for (int i = -3; i < 0; i++) {
Button alertButton = alertDialog.getButton(i);
if (alertButton != null && alertButton.getPaddingStart() > dim5dp) {
alertButton.setPadding(dim5dp, dim5dp, dim5dp, dim5dp);
}, "androidacy_repo")
val dim5dp = FoxDisplay.dpToPixel(5f)
builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp)
activity.runOnUiThread {
val alertDialog = builder.show()
for (i in -3..-1) {
val alertButton = alertDialog.getButton(i)
if (alertButton != null && alertButton.paddingStart > dim5dp) {
alertButton.setPadding(dim5dp, dim5dp, dim5dp, dim5dp)
}
}
});
}
}
void notifyCompatModeRaw(int value) {
if (this.consumedAction)
return;
if (BuildConfig.DEBUG)
Timber.d("Androidacy Compat mode: %s", value);
this.notifiedCompatMode = value;
@Suppress("NAME_SHADOWING")
fun notifyCompatModeRaw(value: Int) {
var value = value
if (consumedAction) return
if (BuildConfig.DEBUG) Timber.d("Androidacy Compat mode: %s", value)
notifiedCompatMode = value
if (value < 0) {
value = 0;
value = 0
} else if (value > MAX_COMPAT_MODE) {
value = MAX_COMPAT_MODE;
value = MAX_COMPAT_MODE
}
this.effectiveCompatMode = value;
effectiveCompatMode = value
}
@JavascriptInterface
public void forceQuit(String error) {
fun forceQuit(error: String?) {
// Allow forceQuit and cancel in downloadMode
if (this.consumedAction && !this.downloadMode)
return;
this.consumedAction = true;
this.forceQuitRaw(error);
if (consumedAction && !downloadMode) return
consumedAction = true
forceQuitRaw(error)
}
@JavascriptInterface
public void cancel() {
fun cancel() {
// Allow forceQuit and cancel in downloadMode
if (this.consumedAction && !this.downloadMode)
return;
this.consumedAction = true;
this.activity.runOnUiThread(this.activity::forceBackPressed);
if (consumedAction && !downloadMode) return
consumedAction = true
activity.runOnUiThread { activity.forceBackPressed() }
}
/**
* Open an url always in an external page or browser.
*/
@JavascriptInterface
public void openUrl(String url) {
if (this.consumedAction)
return;
this.consumedAction = true;
this.downloadMode = false;
if (BuildConfig.DEBUG)
Timber.d("Received openUrl request: %s", url);
if (Objects.equals(Uri.parse(url).getScheme(), "https")) {
IntentHelper.Companion.openUrl(this.activity, url);
fun openUrl(url: String?) {
if (consumedAction) return
consumedAction = true
downloadMode = false
if (BuildConfig.DEBUG) Timber.d("Received openUrl request: %s", url)
if (Uri.parse(url).scheme == "https") {
openUrl(activity, url)
}
}
@ -193,93 +214,91 @@ public class AndroidacyWebAPI {
* Open an url in a custom tab if possible.
*/
@JavascriptInterface
public void openCustomTab(String url) {
if (this.consumedAction)
return;
this.consumedAction = true;
this.downloadMode = false;
if (BuildConfig.DEBUG)
Timber.d("Received openCustomTab request: %s", url);
if (Objects.equals(Uri.parse(url).getScheme(), "https")) {
IntentHelper.openCustomTab(this.activity, url);
fun openCustomTab(url: String?) {
if (consumedAction) return
consumedAction = true
downloadMode = false
if (BuildConfig.DEBUG) Timber.d("Received openCustomTab request: %s", url)
if (Uri.parse(url).scheme == "https") {
openCustomTab(activity, url)
}
}
/**
* Return if current theme is a light theme.
*/
@JavascriptInterface
public boolean isLightTheme() {
return MainApplication.getINSTANCE().isLightTheme();
}
@get:JavascriptInterface
val isLightTheme: Boolean
get() = MainApplication.getINSTANCE().isLightTheme
/**
* Check if the manager has received root access
* (Note: hasRoot only return true on Magisk rooted phones)
*/
@JavascriptInterface
public boolean hasRoot() {
return InstallerInitializer.peekMagiskPath() != null;
fun hasRoot(): Boolean {
return peekMagiskPath() != null
}
/**
* Check if the install API can be used
*/
@JavascriptInterface
public boolean canInstall() {
fun canInstall(): Boolean {
// With lockdown mode enabled or lack of root, install should not have any effect
return this.allowInstall && this.hasRoot() && !MainApplication.isShowcaseMode();
return allowInstall && hasRoot() && !MainApplication.isShowcaseMode()
}
/**
* install a module via url, with the file checked with the md5 checksum value.
*/
@JavascriptInterface
public void install(String moduleUrl, String installTitle, String checksum) {
fun install(moduleUrl: String, installTitle: String?, checksum: String?) {
// If compat mode is 0, this means Androidacy didn't implemented a download mode yet
if (this.consumedAction || (this.effectiveCompatMode >= 1 && !this.canInstall())) {
return;
var installTitle = installTitle
var checksum = checksum
if (consumedAction || effectiveCompatMode >= 1 && !canInstall()) {
return
}
this.consumedAction = true;
this.downloadMode = false;
if (BuildConfig.DEBUG)
Timber.d("Received install request: " + moduleUrl + " " + installTitle + " " + checksum);
if (!AndroidacyUtil.isAndroidacyLink(moduleUrl)) {
this.forceQuitRaw("Non Androidacy module link used on Androidacy");
return;
consumedAction = true
downloadMode = false
if (BuildConfig.DEBUG) Timber.d("Received install request: $moduleUrl $installTitle $checksum")
if (!isAndroidacyLink(moduleUrl)) {
forceQuitRaw("Non Androidacy module link used on Androidacy")
return
}
checksum = Hashes.checkSumFormat(checksum);
if (checksum == null || checksum.isEmpty()) {
Timber.w("Androidacy didn't provided a checksum!");
} else if (!Hashes.checkSumValid(checksum)) {
this.forceQuitRaw("Androidacy didn't provided a valid checksum");
return;
checksum = checkSumFormat(checksum)
if (checksum.isNullOrEmpty()) {
Timber.w("Androidacy didn't provided a checksum!")
} else if (!checkSumValid(checksum)) {
forceQuitRaw("Androidacy didn't provided a valid checksum")
return
}
// moduleId is the module parameter in the url
String moduleId = AndroidacyUtil.getModuleId(moduleUrl);
val moduleId = getModuleId(moduleUrl)
// Let's handle download mode ourself if not implemented
if (this.effectiveCompatMode < 1) {
if (!this.canInstall()) {
this.downloadMode = true;
this.activity.runOnUiThread(() -> {
if (this.activity.webView != null) {
this.activity.webView.loadUrl(moduleUrl);
if (effectiveCompatMode < 1) {
if (!canInstall()) {
downloadMode = true
activity.runOnUiThread {
if (activity.webView != null) {
activity.webView!!.loadUrl(moduleUrl)
}
});
}
} else {
this.openNativeModuleDialogRaw(moduleUrl, moduleId, installTitle, checksum, true);
openNativeModuleDialogRaw(moduleUrl, moduleId, installTitle, checksum, true)
}
} else {
RepoModule repoModule = AndroidacyRepoData.getInstance().moduleHashMap.get(installTitle);
String config = null;
boolean mmtReborn = false;
if (repoModule != null && Objects.requireNonNull(repoModule.moduleInfo.name).length() >= 3) {
installTitle = repoModule.moduleInfo.name; // Set title to module name
config = repoModule.moduleInfo.config;
mmtReborn = repoModule.moduleInfo.mmtReborn;
val repoModule = AndroidacyRepoData.getInstance().moduleHashMap[installTitle]
var config: String? = null
var mmtReborn = false
if (repoModule != null && Objects.requireNonNull<String?>(repoModule.moduleInfo.name).length >= 3) {
installTitle = repoModule.moduleInfo.name // Set title to module name
config = repoModule.moduleInfo.config
mmtReborn = repoModule.moduleInfo.mmtReborn
}
this.activity.backOnResume = true;
IntentHelper.openInstaller(this.activity, moduleUrl, installTitle, config, checksum, mmtReborn);
activity.backOnResume = true
openInstaller(activity, moduleUrl, installTitle, config, checksum, mmtReborn)
}
}
@ -287,74 +306,73 @@ public class AndroidacyWebAPI {
* install a module via url, with the file checked with the md5 checksum value.
*/
@JavascriptInterface
public void openNativeModuleDialog(String moduleUrl, String moduleId, String checksum) {
if (this.consumedAction)
return;
this.consumedAction = true;
this.downloadMode = false;
if (!AndroidacyUtil.isAndroidacyLink(moduleUrl)) {
this.forceQuitRaw("Non Androidacy module link used on Androidacy");
return;
fun openNativeModuleDialog(moduleUrl: String?, moduleId: String?, checksum: String?) {
var checksum = checksum
if (consumedAction) return
consumedAction = true
downloadMode = false
if (!isAndroidacyLink(moduleUrl)) {
forceQuitRaw("Non Androidacy module link used on Androidacy")
return
}
checksum = Hashes.checkSumFormat(checksum);
if (checksum == null || checksum.isEmpty()) {
Timber.w("Androidacy WebView didn't provided a checksum!");
} else if (!Hashes.checkSumValid(checksum)) {
this.forceQuitRaw("Androidacy didn't provided a valid checksum");
return;
checksum = checkSumFormat(checksum)
if (checksum.isNullOrEmpty()) {
Timber.w("Androidacy WebView didn't provided a checksum!")
} else if (!checkSumValid(checksum)) {
forceQuitRaw("Androidacy didn't provided a valid checksum")
return
}
// Get moduleTitle from url
String moduleTitle = AndroidacyUtil.getModuleTitle(moduleUrl);
this.openNativeModuleDialogRaw(moduleUrl, moduleId, moduleTitle, checksum, this.canInstall());
val moduleTitle = getModuleTitle(moduleUrl!!)
openNativeModuleDialogRaw(moduleUrl, moduleId, moduleTitle, checksum, canInstall())
}
/**
* Tell if the moduleId is installed on the device
*/
@JavascriptInterface
public boolean isModuleInstalled(String moduleId) {
return Objects.requireNonNull(ModuleManager.getInstance()).getModules().get(moduleId) != null;
fun isModuleInstalled(moduleId: String?): Boolean {
return instance?.modules?.get(moduleId) != null
}
/**
* Tell if the moduleId is updating and waiting a reboot to update
*/
@JavascriptInterface
public boolean isModuleUpdating(String moduleId) {
LocalModuleInfo localModuleInfo = Objects.requireNonNull(ModuleManager.getInstance()).getModules().get(moduleId);
return localModuleInfo != null && localModuleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING);
fun isModuleUpdating(moduleId: String?): Boolean {
val localModuleInfo = instance?.modules?.get(moduleId)
return localModuleInfo != null && localModuleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING)
}
/**
* Return the module version name or null if not installed.
*/
@JavascriptInterface
public String getModuleVersion(String moduleId) {
LocalModuleInfo localModuleInfo = Objects.requireNonNull(ModuleManager.getInstance()).getModules().get(moduleId);
return localModuleInfo != null ? localModuleInfo.version : null;
fun getModuleVersion(moduleId: String?): String? {
val localModuleInfo = instance?.modules?.get(moduleId)
return localModuleInfo?.version
}
/**
* Return the module version code or -1 if not installed.
*/
@JavascriptInterface
public long getModuleVersionCode(String moduleId) {
LocalModuleInfo localModuleInfo = Objects.requireNonNull(ModuleManager.getInstance()).getModules().get(moduleId);
return localModuleInfo != null ? localModuleInfo.versionCode : -1L;
fun getModuleVersionCode(moduleId: String?): Long {
val localModuleInfo = instance?.modules?.get(moduleId)
return localModuleInfo?.versionCode ?: -1L
}
/**
* Hide action bar if visible, the action bar is only visible by default on notes.
*/
@JavascriptInterface
public void hideActionBar() {
if (this.consumedAction)
return;
this.consumedAction = true;
this.activity.runOnUiThread(() -> {
this.activity.hideActionBar();
this.consumedAction = false;
});
fun hideActionBar() {
if (consumedAction) return
consumedAction = true
activity.runOnUiThread {
activity.hideActionBar()
consumedAction = false
}
}
/**
@ -362,27 +380,27 @@ public class AndroidacyWebAPI {
* Optional title param to set action bar title.
*/
@JavascriptInterface
public void showActionBar(final String title) {
if (this.consumedAction)
return;
this.consumedAction = true;
this.activity.runOnUiThread(() -> {
this.activity.showActionBar();
if (title != null && !title.isEmpty()) {
this.activity.setTitle(title);
fun showActionBar(title: String?) {
if (consumedAction) return
consumedAction = true
activity.runOnUiThread {
activity.showActionBar()
if (!title.isNullOrEmpty()) {
activity.title = title
}
this.consumedAction = false;
});
consumedAction = false
}
}
/**
* Return true if the module is an Androidacy module.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
@JavascriptInterface
public boolean isAndroidacyModule(String moduleId) {
LocalModuleInfo localModuleInfo = Objects.requireNonNull(ModuleManager.getInstance()).getModules().get(moduleId);
return localModuleInfo != null && ("Androidacy".equals(localModuleInfo.author) || AndroidacyUtil.isAndroidacyLink(localModuleInfo.config));
fun isAndroidacyModule(moduleId: String?): Boolean {
val localModuleInfo = instance?.modules?.get(moduleId)
return localModuleInfo != null && ("Androidacy" == localModuleInfo.author || isAndroidacyLink(
localModuleInfo.config
))
}
/**
@ -390,20 +408,18 @@ public class AndroidacyWebAPI {
* an Androidacy module or if file doesn't exists.
*/
@JavascriptInterface
public String getAndroidacyModuleFile(String moduleId, String moduleFile) {
moduleId = moduleId.replaceAll("\\.", "").replaceAll("/", "");
if (moduleFile == null || this.consumedAction || !this.isAndroidacyModule(moduleId))
return "";
moduleFile = moduleFile.replaceAll("\\.", "").replaceAll("/", "");
File moduleFolder = new File("/data/adb/modules/" + moduleId);
File absModuleFile = new File(moduleFolder, moduleFile).getAbsoluteFile();
if (!absModuleFile.getPath().startsWith(moduleFolder.getPath()))
return "";
try {
return new String(Files.readSU(absModuleFile.getAbsoluteFile()), StandardCharsets.UTF_8);
} catch (
IOException e) {
return "";
fun getAndroidacyModuleFile(moduleId: String, moduleFile: String?): String {
var moduleId = moduleId
var moduleFile = moduleFile
moduleId = moduleId.replace("\\.".toRegex(), "").replace("/".toRegex(), "")
if (moduleFile == null || consumedAction || !isAndroidacyModule(moduleId)) return ""
moduleFile = moduleFile.replace("\\.".toRegex(), "").replace("/".toRegex(), "")
val moduleFolder = File("/data/adb/modules/$moduleId")
val absModuleFile = File(moduleFolder, moduleFile).absoluteFile
return if (!absModuleFile.path.startsWith(moduleFolder.path)) "" else try {
String(readSU(absModuleFile.absoluteFile), StandardCharsets.UTF_8)
} catch (e: IOException) {
""
}
}
@ -412,139 +428,136 @@ public class AndroidacyWebAPI {
* Return true if action succeeded
*/
@JavascriptInterface
public boolean setAndroidacyModuleMeta(String moduleId, String content) {
moduleId = moduleId.replaceAll("\\.", "").replaceAll("/", "");
if (content == null || this.consumedAction || !this.isAndroidacyModule(moduleId))
return false;
File androidacyMetaFile = new File("/data/adb/modules/" + moduleId + "/.androidacy");
try {
Files.writeSU(androidacyMetaFile, content.getBytes(StandardCharsets.UTF_8));
return true;
} catch (
IOException e) {
return false;
fun setAndroidacyModuleMeta(moduleId: String, content: String?): Boolean {
var moduleId = moduleId
moduleId = moduleId.replace("\\.".toRegex(), "").replace("/".toRegex(), "")
if (content == null || consumedAction || !isAndroidacyModule(moduleId)) return false
val androidacyMetaFile = File("/data/adb/modules/$moduleId/.androidacy")
return try {
writeSU(androidacyMetaFile, content.toByteArray(StandardCharsets.UTF_8))
true
} catch (e: IOException) {
false
}
}
/**
* Return current app version code
*/
@JavascriptInterface
public int getAppVersionCode() {
return BuildConfig.VERSION_CODE;
}
@get:JavascriptInterface
val appVersionCode: Int
get() = BuildConfig.VERSION_CODE
/**
* Return current app version name
*/
@JavascriptInterface
public String getAppVersionName() {
return BuildConfig.VERSION_NAME;
}
@get:JavascriptInterface
val appVersionName: String
get() = BuildConfig.VERSION_NAME
/**
* Return current magisk version code or 0 if not applicable
*/
@JavascriptInterface
public int getMagiskVersionCode() {
return InstallerInitializer.peekMagiskPath() == null ? 0 : InstallerInitializer.peekMagiskVersion();
}
@get:JavascriptInterface
val magiskVersionCode: Int
get() = if (peekMagiskPath() == null) 0 else peekMagiskVersion()
/**
* Return current android sdk-int version code, see:
* <a href="https://source.android.com/setup/start/build-numbers">right here</a>
* [right here](https://source.android.com/setup/start/build-numbers)
*/
@JavascriptInterface
public int getAndroidVersionCode() {
return Build.VERSION.SDK_INT;
}
@get:JavascriptInterface
val androidVersionCode: Int
get() = Build.VERSION.SDK_INT
/**
* Return current navigation bar height or 0 if not visible
*/
@JavascriptInterface
public int getNavigationBarHeight() {
return this.activity.getNavigationBarHeight();
}
/**
* Allow Androidacy backend to notify compat mode
* return current effective compat mode
*/
@JavascriptInterface
public int getEffectiveCompatMode() {
return this.effectiveCompatMode;
}
@get:JavascriptInterface
val navigationBarHeight: Int
get() = activity.navigationBarHeight
/**
* Return current theme accent color
*/
@JavascriptInterface
public int getAccentColor() {
Resources.Theme theme = this.activity.getTheme();
TypedValue typedValue = new TypedValue();
theme.resolveAttribute(androidx.appcompat.R.attr.colorPrimary, typedValue, true);
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
return typedValue.data;
@get:JavascriptInterface
val accentColor: Int
get() {
val theme = activity.theme
val typedValue = TypedValue()
theme.resolveAttribute(androidx.appcompat.R.attr.colorPrimary, typedValue, true)
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
return typedValue.data
}
theme.resolveAttribute(android.R.attr.colorAccent, typedValue, true)
return typedValue.data
}
theme.resolveAttribute(android.R.attr.colorAccent, typedValue, true);
return typedValue.data;
}
/**
* Return current theme foreground color
*/
@JavascriptInterface
public int getForegroundColor() {
return this.activity.isLightTheme() ? Color.BLACK : Color.WHITE;
}
@get:JavascriptInterface
val foregroundColor: Int
get() = if (activity.isLightTheme) Color.BLACK else Color.WHITE
/**
* Return current theme background color
*/
@JavascriptInterface
public int getBackgroundColor() {
Resources.Theme theme = this.activity.getTheme();
TypedValue typedValue = new TypedValue();
theme.resolveAttribute(com.google.android.material.R.attr.backgroundColor, typedValue, true);
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
return typedValue.data;
@get:JavascriptInterface
val backgroundColor: Int
get() {
val theme = activity.theme
val typedValue = TypedValue()
theme.resolveAttribute(
com.google.android.material.R.attr.backgroundColor,
typedValue,
true
)
if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
return typedValue.data
}
theme.resolveAttribute(android.R.attr.background, typedValue, true)
return typedValue.data
}
theme.resolveAttribute(android.R.attr.background, typedValue, true);
return typedValue.data;
}
/**
* Return current hex string of monet theme
*/
@JavascriptInterface
public String getMonetColor(String id) {
@SuppressLint("DiscouragedApi") int nameResourceID = this.activity.getResources().getIdentifier("@android:color/" + id, "color", this.activity.getApplicationInfo().packageName);
if (nameResourceID == 0) {
throw new IllegalArgumentException("No resource string found with name " + id);
fun getMonetColor(id: String): String {
@SuppressLint("DiscouragedApi") val nameResourceID = activity.resources.getIdentifier(
"@android:color/$id", "color", activity.applicationInfo.packageName
)
return if (nameResourceID == 0) {
throw IllegalArgumentException("No resource string found with name $id")
} else {
int color = ContextCompat.getColor(this.activity, nameResourceID);
int red = Color.red(color);
int blue = Color.blue(color);
int green = Color.green(color);
return String.format("#%02x%02x%02x", red, green, blue);
val color = ContextCompat.getColor(activity, nameResourceID)
val red = Color.red(color)
val blue = Color.blue(color)
val green = Color.green(color)
String.format("#%02x%02x%02x", red, green, blue)
}
}
@JavascriptInterface
public void setAndroidacyToken(String token) {
AndroidacyRepoData.getInstance().setToken(token);
fun setAndroidacyToken(token: String?) {
AndroidacyRepoData.getInstance().setToken(token)
}
// Androidacy feature level declaration method
@JavascriptInterface
public void notifyCompatUnsupported() {
this.notifyCompatModeRaw(COMPAT_UNSUPPORTED);
fun notifyCompatUnsupported() {
notifyCompatModeRaw(COMPAT_UNSUPPORTED)
}
@JavascriptInterface
public void notifyCompatDownloadButton() {
this.notifyCompatModeRaw(COMPAT_DOWNLOAD);
fun notifyCompatDownloadButton() {
notifyCompatModeRaw(COMPAT_DOWNLOAD)
}
companion object {
const val COMPAT_UNSUPPORTED = 0
const val COMPAT_DOWNLOAD = 1
private const val MAX_COMPAT_MODE = 1
}
}
}

@ -64,7 +64,7 @@ class ModuleManager private constructor() : SyncManager() {
// get all dirs under the realms/repos/ dir under app's data dir
val cacheRoot =
File(MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/").toURI())
var moduleListCache: ModuleListCache
var moduleListCache: ModuleListCache?
for (dir in Objects.requireNonNull<Array<File>>(cacheRoot.listFiles())) {
if (dir.isDirectory) {
// if the dir name matches the module name, use it as the cache dir
@ -83,25 +83,27 @@ class ModuleManager private constructor() : SyncManager() {
)
moduleListCache =
realm.where(ModuleListCache::class.java).equalTo("codename", module)
.findFirst()!!
.findFirst()
Timber.d("Found cache for %s", module)
// get module info from cache
if (moduleInfo == null) {
moduleInfo = LocalModuleInfo(module)
}
moduleInfo.name =
if (moduleListCache.name != "") moduleListCache.name else module
moduleInfo.description =
if (moduleListCache.description != "") moduleListCache.description else null
moduleInfo.author =
if (moduleListCache.author != "") moduleListCache.author else null
moduleInfo.safe = moduleListCache.isSafe == true
moduleInfo.support =
if (moduleListCache.support != "") moduleListCache.support else null
moduleInfo.donate =
if (moduleListCache.donate != "") moduleListCache.donate else null
moduleInfo.flags = moduleInfo.flags or FLAG_MM_REMOTE_MODULE
moduleInfos[module] = moduleInfo
if (moduleListCache != null) {
moduleInfo.name =
if (moduleListCache.name != "") moduleListCache.name else module
moduleInfo.description =
if (moduleListCache.description != "") moduleListCache.description else moduleInfo.description
moduleInfo.author =
if (moduleListCache.author != "") moduleListCache.author else moduleInfo.author
moduleInfo.safe = moduleListCache.isSafe == true
moduleInfo.support =
if (moduleListCache.support != "") moduleListCache.support else null
moduleInfo.donate =
if (moduleListCache.donate != "") moduleListCache.donate else null
moduleInfo.flags = moduleInfo.flags or FLAG_MM_REMOTE_MODULE
moduleInfos[module] = moduleInfo
}
realm.close()
break
}

@ -2,6 +2,7 @@ package com.fox2code.mmm.module;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.Spanned;
import android.widget.Button;
@ -52,7 +53,7 @@ public enum ActionButtonType {
}
TrackHelper.track().event("view_notes", name).with(MainApplication.getINSTANCE().getTracker());
String notesUrl = moduleHolder.repoModule.notesUrl;
if (AndroidacyUtil.isAndroidacyLink(notesUrl)) {
if (AndroidacyUtil.Companion.isAndroidacyLink(notesUrl)) {
IntentHelper.openUrlAndroidacy(button.getContext(), notesUrl, false, moduleHolder.repoModule.moduleInfo.name, moduleHolder.getMainModuleConfig());
} else {
IntentHelper.openMarkdown(button.getContext(), notesUrl, moduleHolder.repoModule.moduleInfo.name, moduleHolder.getMainModuleConfig(), moduleHolder.repoModule.moduleInfo.changeBoot, moduleHolder.repoModule.moduleInfo.needRamdisk, moduleHolder.repoModule.moduleInfo.minMagisk, moduleHolder.repoModule.moduleInfo.minApi, moduleHolder.repoModule.moduleInfo.maxApi);
@ -94,10 +95,28 @@ public enum ActionButtonType {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("view_update_install", name).with(MainApplication.getINSTANCE().getTracker());
// if icon is reinstall, we need to uninstall first - warn the user but don't proceed
if (moduleHolder.moduleInfo != null) {
// get icon of the button
Drawable icon = button.getChipIcon();
if (icon != null && icon.getConstantState() != null) {
Drawable reinstallIcon = button.getContext().getDrawable(R.drawable.ic_baseline_refresh_24);
if (reinstallIcon != null && reinstallIcon.getConstantState() != null) {
if (icon.getConstantState().equals(reinstallIcon.getConstantState())) {
new MaterialAlertDialogBuilder(button.getContext())
.setTitle(R.string.reinstall)
.setMessage(R.string.reinstall_warning)
.setPositiveButton(R.string.reinstall, null)
.show();
return;
}
}
}
}
String updateZipUrl = moduleHolder.getUpdateZipUrl();
if (updateZipUrl == null) return;
// Androidacy manage the selection between download and install
if (AndroidacyUtil.isAndroidacyLink(updateZipUrl)) {
if (AndroidacyUtil.Companion.isAndroidacyLink(updateZipUrl)) {
IntentHelper.openUrlAndroidacy(button.getContext(), updateZipUrl, true, moduleInfo.name, moduleInfo.config);
return;
}
@ -211,7 +230,7 @@ public enum ActionButtonType {
name = moduleHolder.repoModule.moduleInfo.name;
}
TrackHelper.track().event("config_module", name).with(MainApplication.getINSTANCE().getTracker());
if (AndroidacyUtil.isAndroidacyLink(config)) {
if (AndroidacyUtil.Companion.isAndroidacyLink(config)) {
IntentHelper.openUrlAndroidacy(button.getContext(), config, true);
} else {
IntentHelper.openConfig(button.getContext(), config);

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

@ -1,49 +1,70 @@
package com.fox2code.mmm.repo;
package com.fox2code.mmm.repo
import androidx.annotation.StringRes;
import androidx.annotation.StringRes
import com.fox2code.mmm.manager.ModuleInfo
import com.fox2code.mmm.manager.ModuleInfo;
class RepoModule {
@JvmField
val repoData: RepoData
@JvmField
val moduleInfo: ModuleInfo
@JvmField
val id: String
@JvmField
var repoName: String? = null
@JvmField
var lastUpdated: Long = 0
@JvmField
var propUrl: String? = null
@JvmField
var zipUrl: String? = null
@JvmField
var notesUrl: String? = null
@JvmField
var checksum: String? = null
@JvmField
var processed = false
public class RepoModule {
public final RepoData repoData;
public final ModuleInfo moduleInfo;
public final String id;
public String repoName;
public long lastUpdated;
public String propUrl;
public String zipUrl;
public String notesUrl;
public String checksum;
public boolean processed;
@JvmField
@StringRes
public int qualityText;
public int qualityValue;
public boolean safe;
var qualityText = 0
@JvmField
var qualityValue = 0
var safe: Boolean
public RepoModule(RepoData repoData, String id) {
this.repoData = repoData;
this.moduleInfo = new ModuleInfo(id);
this.id = id;
this.moduleInfo.flags |=
ModuleInfo.FLAG_METADATA_INVALID;
this.safe = this.moduleInfo.safe;
constructor(repoData: RepoData, id: String) {
this.repoData = repoData
moduleInfo = ModuleInfo(id)
this.id = id
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_METADATA_INVALID
safe = moduleInfo.safe
}
// allows all fields to be set-
public RepoModule(RepoData repoData, String id, String name, String description, String author, String donate, String config, String support, String version, int versionCode) {
this.repoData = repoData;
this.moduleInfo = new ModuleInfo(id);
this.id = id;
this.moduleInfo.name = name;
this.moduleInfo.description = description;
this.moduleInfo.author = author;
this.moduleInfo.donate = donate;
this.moduleInfo.config = config;
this.moduleInfo.support = support;
this.moduleInfo.version = version;
this.moduleInfo.versionCode = versionCode;
this.moduleInfo.flags |=
ModuleInfo.FLAG_METADATA_INVALID;
this.safe = this.moduleInfo.safe;
constructor(
repoData: RepoData,
id: String,
name: String?,
description: String?,
author: String?,
donate: String?,
config: String?,
support: String?,
version: String?,
versionCode: Int
) {
this.repoData = repoData
moduleInfo = ModuleInfo(id)
this.id = id
moduleInfo.name = name
moduleInfo.description = description
moduleInfo.author = author
moduleInfo.donate = donate
moduleInfo.config = config
moduleInfo.support = support
moduleInfo.version = version
moduleInfo.versionCode = versionCode.toLong()
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_METADATA_INVALID
safe = moduleInfo.safe
}
}
}

@ -1,423 +1,460 @@
package com.fox2code.mmm.utils.io;
@file:Suppress("unused")
import static com.fox2code.mmm.AppUpdateManager.FLAG_COMPAT_LOW_QUALITY;
import static com.fox2code.mmm.AppUpdateManager.getFlagsForModule;
package com.fox2code.mmm.utils.io
import android.os.Build;
import android.text.TextUtils;
import android.os.Build
import android.text.TextUtils
import com.fox2code.mmm.AppUpdateManager
import com.fox2code.mmm.manager.ModuleInfo
import com.topjohnwu.superuser.io.SuFileInputStream
import timber.log.Timber
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.manager.ModuleInfo;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
enum class PropUtils {
;
import timber.log.Timber;
@Suppress("SpellCheckingInspection")
companion object {
private val moduleSupportsFallbacks = HashMap<String, String>()
private val moduleConfigsFallbacks = HashMap<String, String>()
private val moduleMinApiFallbacks = HashMap<String, Int>()
private val moduleUpdateJsonFallbacks = HashMap<String, String>()
private val moduleMTTRebornFallback = HashSet<String>()
private val moduleImportantProp = HashSet(
mutableListOf(
"id", "name", "version", "versionCode"
)
)
private var RIRU_MIN_API = 0
@SuppressWarnings("SpellCheckingInspection")
public enum PropUtils {
;
private static final HashMap<String, String> moduleSupportsFallbacks = new HashMap<>();
private static final HashMap<String, String> moduleConfigsFallbacks = new HashMap<>();
private static final HashMap<String, Integer> moduleMinApiFallbacks = new HashMap<>();
private static final HashMap<String, String> moduleUpdateJsonFallbacks = new HashMap<>();
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private static final HashSet<String> moduleMTTRebornFallback = new HashSet<>();
private static final HashSet<String> moduleImportantProp = new HashSet<>(Arrays.asList(
"id", "name", "version", "versionCode"
));
private static final int RIRU_MIN_API;
// Note: These fallback values may not be up-to-date
// They are only used if modules don't define the metadata
static {
// Support are pages or groups where the user can get support for the module
moduleSupportsFallbacks.put("aospill", "https://t.me/PannekoX");
moduleSupportsFallbacks.put("bromitewebview", "https://t.me/androidacy_discussions");
moduleSupportsFallbacks.put("fontrevival", "https://t.me/androidacy_discussions");
moduleSupportsFallbacks.put("MagiskHidePropsConf", "https://forum.xda-developers.com/t" +
"/module-magiskhide-props-config-safetynet-prop-edits-and-more-v6-1-1.3789228/");
moduleSupportsFallbacks.put("quickstepswitcher", "https://t.me/QuickstepSwitcherSupport");
moduleSupportsFallbacks.put("riru_edxposed", "https://t.me/EdXposed");
moduleSupportsFallbacks.put("riru_lsposed", "https://github.com/LSPosed/LSPosed/issues");
moduleSupportsFallbacks.put("substratum", "https://github.com/substratum/substratum/issues");
// Config are application installed by modules that allow them to be configured
moduleConfigsFallbacks.put("quickstepswitcher", "xyz.paphonb.quickstepswitcher");
moduleConfigsFallbacks.put("hex_installer_module", "project.vivid.hex.bodhi");
moduleConfigsFallbacks.put("riru_edxposed", "org.meowcat.edxposed.manager");
moduleConfigsFallbacks.put("riru_lsposed", "org.lsposed.manager");
moduleConfigsFallbacks.put("zygisk_lsposed", "org.lsposed.manager");
moduleConfigsFallbacks.put("xposed_dalvik", "de.robv.android.xposed.installer");
moduleConfigsFallbacks.put("xposed", "de.robv.android.xposed.installer");
moduleConfigsFallbacks.put("substratum", "projekt.substratum");
// minApi is the minimum android version required to use the module
moduleMinApiFallbacks.put("HideNavBar", Build.VERSION_CODES.Q);
moduleMinApiFallbacks.put("riru_ifw_enhance", Build.VERSION_CODES.O);
moduleMinApiFallbacks.put("zygisk_ifw_enhance", Build.VERSION_CODES.O);
moduleMinApiFallbacks.put("riru_edxposed", Build.VERSION_CODES.O);
moduleMinApiFallbacks.put("zygisk_edxposed", Build.VERSION_CODES.O);
moduleMinApiFallbacks.put("riru_lsposed", Build.VERSION_CODES.O_MR1);
moduleMinApiFallbacks.put("zygisk_lsposed", Build.VERSION_CODES.O_MR1);
moduleMinApiFallbacks.put("noneDisplayCutout", Build.VERSION_CODES.P);
moduleMinApiFallbacks.put("quickstepswitcher", Build.VERSION_CODES.P);
moduleMinApiFallbacks.put("riru_clipboard_whitelist", Build.VERSION_CODES.Q);
// minApi for riru core include submodules
moduleMinApiFallbacks.put("riru-core", RIRU_MIN_API = Build.VERSION_CODES.M);
// Fallbacks in case updateJson is missing
final String GH_UC = "https://raw.githubusercontent.com/";
moduleUpdateJsonFallbacks.put("BluetoothLibraryPatcher",
GH_UC + "3arthur6/BluetoothLibraryPatcher/master/update.json");
moduleUpdateJsonFallbacks.put("Detach",
GH_UC + "xerta555/Detach-Files/blob/master/Updater.json");
for (String module : new String[]{"busybox-ndk", "adb-ndk", "twrp-keep",
"adreno-dev", "nano-ndk", "zipsigner", "nexusmedia", "mtd-ndk"}) {
moduleUpdateJsonFallbacks.put(module,
GH_UC + "Magisk-Modules-Repo/" + module + "/master/update.json");
// Note: These fallback values may not be up-to-date
// They are only used if modules don't define the metadata
init {
// Support are pages or groups where the user can get support for the module
moduleSupportsFallbacks["aospill"] = "https://t.me/PannekoX"
moduleSupportsFallbacks["bromitewebview"] = "https://t.me/androidacy_discussions"
moduleSupportsFallbacks["fontrevival"] = "https://t.me/androidacy_discussions"
moduleSupportsFallbacks["MagiskHidePropsConf"] = "https://forum.xda-developers.com/t" +
"/module-magiskhide-props-config-safetynet-prop-edits-and-more-v6-1-1.3789228/"
moduleSupportsFallbacks["quickstepswitcher"] = "https://t.me/QuickstepSwitcherSupport"
moduleSupportsFallbacks["riru_edxposed"] = "https://t.me/EdXposed"
moduleSupportsFallbacks["riru_lsposed"] = "https://github.com/LSPosed/LSPosed/issues"
moduleSupportsFallbacks["substratum"] =
"https://github.com/substratum/substratum/issues"
// Config are application installed by modules that allow them to be configured
moduleConfigsFallbacks["quickstepswitcher"] = "xyz.paphonb.quickstepswitcher"
moduleConfigsFallbacks["hex_installer_module"] = "project.vivid.hex.bodhi"
moduleConfigsFallbacks["riru_edxposed"] = "org.meowcat.edxposed.manager"
moduleConfigsFallbacks["riru_lsposed"] = "org.lsposed.manager"
moduleConfigsFallbacks["zygisk_lsposed"] = "org.lsposed.manager"
moduleConfigsFallbacks["xposed_dalvik"] = "de.robv.android.xposed.installer"
moduleConfigsFallbacks["xposed"] = "de.robv.android.xposed.installer"
moduleConfigsFallbacks["substratum"] = "projekt.substratum"
// minApi is the minimum android version required to use the module
moduleMinApiFallbacks["HideNavBar"] = Build.VERSION_CODES.Q
moduleMinApiFallbacks["riru_ifw_enhance"] = Build.VERSION_CODES.O
moduleMinApiFallbacks["zygisk_ifw_enhance"] = Build.VERSION_CODES.O
moduleMinApiFallbacks["riru_edxposed"] = Build.VERSION_CODES.O
moduleMinApiFallbacks["zygisk_edxposed"] = Build.VERSION_CODES.O
moduleMinApiFallbacks["riru_lsposed"] = Build.VERSION_CODES.O_MR1
moduleMinApiFallbacks["zygisk_lsposed"] = Build.VERSION_CODES.O_MR1
moduleMinApiFallbacks["noneDisplayCutout"] = Build.VERSION_CODES.P
moduleMinApiFallbacks["quickstepswitcher"] = Build.VERSION_CODES.P
moduleMinApiFallbacks["riru_clipboard_whitelist"] = Build.VERSION_CODES.Q
// minApi for riru core include submodules
moduleMinApiFallbacks["riru-core"] = Build.VERSION_CODES.M.also { RIRU_MIN_API = it }
// Fallbacks in case updateJson is missing
val ghUC = "https://raw.githubusercontent.com/"
moduleUpdateJsonFallbacks["BluetoothLibraryPatcher"] =
ghUC + "3arthur6/BluetoothLibraryPatcher/master/update.json"
moduleUpdateJsonFallbacks["Detach"] =
ghUC + "xerta555/Detach-Files/blob/master/Updater.json"
for (module in arrayOf(
"busybox-ndk", "adb-ndk", "twrp-keep",
"adreno-dev", "nano-ndk", "zipsigner", "nexusmedia", "mtd-ndk"
)) {
moduleUpdateJsonFallbacks[module] =
ghUC + "Magisk-Modules-Repo/" + module + "/master/update.json"
}
moduleUpdateJsonFallbacks["riru_ifw_enhance"] = "https://github.com/" +
"Kr328/Riru-IFWEnhance/releases/latest/download/riru-ifw-enhance.json"
moduleUpdateJsonFallbacks["zygisk_ifw_enhance"] = "https://github.com/" +
"Kr328/Riru-IFWEnhance/releases/latest/download/zygisk-ifw-enhance.json"
moduleUpdateJsonFallbacks["riru_lsposed"] =
"https://lsposed.github.io/LSPosed/release/riru.json"
moduleUpdateJsonFallbacks["zygisk_lsposed"] =
"https://lsposed.github.io/LSPosed/release/zygisk.json"
}
moduleUpdateJsonFallbacks.put("riru_ifw_enhance", "https://github.com/" +
"Kr328/Riru-IFWEnhance/releases/latest/download/riru-ifw-enhance.json");
moduleUpdateJsonFallbacks.put("zygisk_ifw_enhance", "https://github.com/" +
"Kr328/Riru-IFWEnhance/releases/latest/download/zygisk-ifw-enhance.json");
moduleUpdateJsonFallbacks.put("riru_lsposed",
"https://lsposed.github.io/LSPosed/release/riru.json");
moduleUpdateJsonFallbacks.put("zygisk_lsposed",
"https://lsposed.github.io/LSPosed/release/zygisk.json");
}
public static void readProperties(ModuleInfo moduleInfo, String file,
boolean local) throws IOException {
readProperties(moduleInfo, SuFileInputStream.open(file), file, local);
}
@Throws(IOException::class)
fun readProperties(
moduleInfo: ModuleInfo, file: String,
local: Boolean
) {
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);
}
@Throws(IOException::class)
fun readProperties(
moduleInfo: ModuleInfo, file: String?,
name: String, local: Boolean
) {
readProperties(moduleInfo, SuFileInputStream.open(file!!), name, local)
}
public static void readProperties(ModuleInfo moduleInfo, InputStream inputStream,
String name, boolean local) throws IOException {
boolean readId = false, readIdSec = false, readName = false,
readVersionCode = false, readVersion = false, readDescription = false,
readUpdateJson = false, invalid = false, readMinApi = false, readMaxApi = false,
readMMTReborn = false;
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
int lineNum = 0;
while ((line = bufferedReader.readLine()) != null) {
if (lineNum == 0 && line.startsWith("\u0000")) {
while (line.startsWith("\u0000"))
line = line.substring(1);
}
lineNum++;
int index = line.indexOf('=');
if (index == -1 || line.startsWith("#"))
continue;
String key = line.substring(0, index);
String value = line.substring(index + 1).trim();
// check if field is defined on the moduleInfo object we are reading
if (moduleInfo.toString().contains(key)) {
continue;
}
// 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 (value.isEmpty() && !moduleImportantProp.contains(key))
continue; // allow empty values to pass.
if (isInvalidValue(value)) {
if (local) {
invalid = true;
continue;
} else throw new IOException("Invalid value for key " + key);
@Throws(IOException::class)
fun readProperties(
moduleInfo: ModuleInfo, inputStream: InputStream?,
name: String, local: Boolean
) {
var readId = false
var readIdSec = false
var readName = false
var readVersionCode = false
var readVersion = false
var readDescription = false
var readUpdateJson = false
var invalid = false
var readMinApi = false
var readMaxApi = false
var readMMTReborn = false
BufferedReader(
InputStreamReader(inputStream, StandardCharsets.UTF_8)
).use { bufferedReader ->
var line: String
var lineNum = 0
while (bufferedReader.readLine().also { line = it } != null) {
if (lineNum == 0 && line.startsWith("\u0000")) {
while (line.startsWith("\u0000")) line = line.substring(1)
}
}
switch (key) {
case "id":
lineNum++
val index = line.indexOf('=')
if (index == -1 || line.startsWith("#")) continue
val key = line.substring(0, index)
val value = line.substring(index + 1).trim { it <= ' ' }
// check if field is defined on the moduleInfo object we are reading
if (moduleInfo.toString().contains(key)) {
continue
}
// name and id have their own implementation
if (isInvalidValue(key)) {
if (local) {
invalid = true
continue
} else throw IOException("Invalid key at line $lineNum")
} else {
if (value.isEmpty() && !moduleImportantProp.contains(key)) continue // allow empty values to pass.
if (isInvalidValue(value)) {
if (local) {
invalid = true;
break;
invalid = true
continue
} else throw IOException("Invalid value for key $key")
}
}
when (key) {
"id" -> {
if (isInvalidValue(value)) {
if (local) {
invalid = true
break
}
throw IOException("Invalid module id!")
}
readId = true
if (moduleInfo.id != value) {
invalid = if (local) {
true
} else {
throw IOException(
name + " has an non matching module id! " +
"(Expected \"" + moduleInfo.id + "\" got \"" + value + "\""
)
}
}
throw new IOException("Invalid module id!");
}
readId = true;
if (!moduleInfo.id.equals(value)) {
if (local) {
invalid = true;
} else {
throw new IOException(name + " has an non matching module id! " +
"(Expected \"" + moduleInfo.id + "\" got \"" + value + "\"");
"name" -> {
if (readName) {
if (local) {
invalid = true
break
} else throw IOException("Duplicate module name!")
}
if (isInvalidValue(value)) {
if (local) {
invalid = true
break
}
throw IOException("Invalid module name!")
}
readName = true
moduleInfo.name = value
if (moduleInfo.id == value) {
readIdSec = true
}
}
break;
case "name":
if (readName) {
if (local) {
invalid = true;
break;
} else throw new IOException("Duplicate module name!");
"version" -> {
readVersion = true
moduleInfo.version = value
}
if (isInvalidValue(value)) {
if (local) {
invalid = true;
break;
"versionCode" -> {
readVersionCode = true
try {
moduleInfo.versionCode = value.toLong()
} catch (e: RuntimeException) {
if (local) {
invalid = true
moduleInfo.versionCode = 0
} else throw e
}
throw new IOException("Invalid module name!");
}
readName = true;
moduleInfo.name = value;
if (moduleInfo.id.equals(value)) {
readIdSec = true;
"author" -> moduleInfo.author =
if (value.endsWith(" development team")) value.substring(
0,
value.length - 17
) else value
"description" -> {
moduleInfo.description = value
readDescription = true
}
"updateJsonAk3" -> {
// Only allow AnyKernel3 helper to use "updateJsonAk3"
if ("ak3-helper" != moduleInfo.id) break
if (isInvalidURL(value)) break
moduleInfo.updateJson = value
readUpdateJson = true
}
break;
case "version":
readVersion = true;
moduleInfo.version = value;
break;
case "versionCode":
readVersionCode = true;
try {
moduleInfo.versionCode = Long.parseLong(value);
} catch (RuntimeException e) {
if (local) {
invalid = true;
moduleInfo.versionCode = 0;
} else throw e;
"updateJson" -> {
if (isInvalidURL(value)) break
moduleInfo.updateJson = value
readUpdateJson = true
}
"changeBoot" -> moduleInfo.changeBoot =
java.lang.Boolean.parseBoolean(value)
"mmtReborn" -> {
moduleInfo.mmtReborn = java.lang.Boolean.parseBoolean(value)
readMMTReborn = true
}
"support" -> {
// Do not accept invalid or too broad support links
if (isInvalidURL(value) || "https://forum.xda-developers.com/" == value) break
moduleInfo.support = value
}
break;
case "author":
moduleInfo.author = value.endsWith(" development team") ?
value.substring(0, value.length() - 17) : value;
break;
case "description":
moduleInfo.description = value;
readDescription = true;
break;
case "updateJsonAk3":
// Only allow AnyKernel3 helper to use "updateJsonAk3"
if (!"ak3-helper".equals(moduleInfo.id)) break;
case "updateJson":
if (isInvalidURL(value)) break;
moduleInfo.updateJson = value;
readUpdateJson = true;
break;
case "changeBoot":
moduleInfo.changeBoot = Boolean.parseBoolean(value);
break;
case "mmtReborn":
moduleInfo.mmtReborn = Boolean.parseBoolean(value);
readMMTReborn = true;
break;
case "support":
// Do not accept invalid or too broad support links
if (isInvalidURL(value) ||
"https://forum.xda-developers.com/".equals(value))
break;
moduleInfo.support = value;
break;
case "donate":
// Do not accept invalid donate links
if (isInvalidURL(value)) break;
moduleInfo.donate = value;
break;
case "config":
moduleInfo.config = value;
break;
case "needRamdisk":
moduleInfo.needRamdisk = Boolean.parseBoolean(value);
break;
case "minMagisk":
try {
int i = value.indexOf('.');
"donate" -> {
// Do not accept invalid donate links
if (isInvalidURL(value)) break
moduleInfo.donate = value
}
"config" -> moduleInfo.config = value
"needRamdisk" -> moduleInfo.needRamdisk =
java.lang.Boolean.parseBoolean(value)
"minMagisk" -> try {
val i = value.indexOf('.')
if (i == -1) {
moduleInfo.minMagisk = Integer.parseInt(value);
moduleInfo.minMagisk = value.toInt()
} else {
moduleInfo.minMagisk = // Allow 24.1 to mean 24100
(Integer.parseInt(value.substring(0, i)) * 1000) +
(Integer.parseInt(value.substring(i + 1)) * 100);
moduleInfo.minMagisk = // Allow 24.1 to mean 24100
value.substring(0, i).toInt() * 1000 + value.substring(i + 1)
.toInt() * 100
}
} catch (e: Exception) {
moduleInfo.minMagisk = 0
}
"minApi" -> {
// Special case for Riru EdXposed because
// minApi don't mean the same thing for them
if ("10" == value) break
try {
moduleInfo.minApi = value.toInt()
readMinApi = true
} catch (e: Exception) {
if (!readMinApi) moduleInfo.minApi = 0
}
} catch (Exception e) {
moduleInfo.minMagisk = 0;
}
break;
case "minApi":
// Special case for Riru EdXposed because
// minApi don't mean the same thing for them
if ("10".equals(value)) break;
case "minSdkVersion": // Improve compatibility
try {
moduleInfo.minApi = Integer.parseInt(value);
readMinApi = true;
} catch (Exception e) {
if (!readMinApi) moduleInfo.minApi = 0;
"minSdkVersion" -> try {
moduleInfo.minApi = value.toInt()
readMinApi = true
} catch (e: Exception) {
if (!readMinApi) moduleInfo.minApi = 0
}
break;
case "maxSdkVersion": // Improve compatibility
case "maxApi":
try {
moduleInfo.maxApi = Integer.parseInt(value);
readMaxApi = true;
} catch (Exception e) {
if (!readMaxApi) moduleInfo.maxApi = 0;
"maxSdkVersion", "maxApi" -> try {
moduleInfo.maxApi = value.toInt()
readMaxApi = true
} catch (e: Exception) {
if (!readMaxApi) moduleInfo.maxApi = 0
}
break;
}
}
}
}
if (!readId) {
if (readIdSec && local) {
// Using the name for module id is not really appropriate, so beautify it a bit
moduleInfo.name = makeNameFromId(moduleInfo.id);
} else if (!local) { // Allow local modules to not declare ids
throw new IOException("Didn't read module id at least once!");
if (!readId) {
if (readIdSec && local) {
// Using the name for module id is not really appropriate, so beautify it a bit
moduleInfo.name = makeNameFromId(moduleInfo.id)
} else if (!local) { // Allow local modules to not declare ids
throw IOException("Didn't read module id at least once!")
}
}
}
if (!readVersionCode) {
if (local) {
invalid = true;
moduleInfo.versionCode = 0;
if (!readVersionCode) {
if (local) {
invalid = true
moduleInfo.versionCode = 0
} else {
throw IOException("Didn't read module versionCode at least once!")
}
}
if (!readName || isInvalidValue(moduleInfo.name)) {
moduleInfo.name = makeNameFromId(moduleInfo.id)
}
if (!readVersion) {
moduleInfo.version = "v" + moduleInfo.versionCode
} else {
throw new IOException("Didn't read module versionCode at least once!");
moduleInfo.version = shortenVersionName(
moduleInfo.version, moduleInfo.versionCode
)
}
if (!readDescription || isInvalidValue(moduleInfo.description)) {
moduleInfo.description = ""
}
if (!readUpdateJson) {
moduleInfo.updateJson = moduleUpdateJsonFallbacks[moduleInfo.id]
}
if (moduleInfo.minApi == 0 || !readMinApi) {
val minApiFallback = moduleMinApiFallbacks[moduleInfo.id]
if (minApiFallback != null) moduleInfo.minApi =
minApiFallback else if (moduleInfo.id.startsWith("riru_")
|| moduleInfo.id.startsWith("riru-")
) moduleInfo.minApi = RIRU_MIN_API
}
if (moduleInfo.support == null) {
moduleInfo.support = moduleSupportsFallbacks[moduleInfo.id]
}
if (moduleInfo.config == null) {
moduleInfo.config = moduleConfigsFallbacks[moduleInfo.id]
}
if (!readMMTReborn) {
moduleInfo.mmtReborn = moduleMTTRebornFallback.contains(moduleInfo.id) ||
AppUpdateManager.getFlagsForModule(moduleInfo.id) and
AppUpdateManager.FLAG_COMPAT_MMT_REBORN != 0
}
// All local modules should have an author
// set to "Unknown" if author is missing.
if (local && moduleInfo.author == null) {
moduleInfo.author = "Unknown"
}
if (invalid) {
moduleInfo.flags = moduleInfo.flags or ModuleInfo.FLAG_METADATA_INVALID
// This shouldn't happen but just in case
if (!local) throw IOException("Invalid properties!")
}
}
if (!readName || isInvalidValue(moduleInfo.name)) {
moduleInfo.name = makeNameFromId(moduleInfo.id);
}
if (!readVersion) {
moduleInfo.version = "v" + moduleInfo.versionCode;
} else {
moduleInfo.version = shortenVersionName(
moduleInfo.version, moduleInfo.versionCode);
}
if (!readDescription || isInvalidValue(moduleInfo.description)) {
moduleInfo.description = "";
}
if (!readUpdateJson) {
moduleInfo.updateJson = moduleUpdateJsonFallbacks.get(moduleInfo.id);
}
if (moduleInfo.minApi == 0 || !readMinApi) {
Integer minApiFallback = moduleMinApiFallbacks.get(moduleInfo.id);
if (minApiFallback != null)
moduleInfo.minApi = minApiFallback;
else if (moduleInfo.id.startsWith("riru_")
|| moduleInfo.id.startsWith("riru-"))
moduleInfo.minApi = RIRU_MIN_API;
}
if (moduleInfo.support == null) {
moduleInfo.support = moduleSupportsFallbacks.get(moduleInfo.id);
}
if (moduleInfo.config == null) {
moduleInfo.config = moduleConfigsFallbacks.get(moduleInfo.id);
}
if (!readMMTReborn) {
moduleInfo.mmtReborn = moduleMTTRebornFallback.contains(moduleInfo.id) ||
(AppUpdateManager.getFlagsForModule(moduleInfo.id) &
AppUpdateManager.FLAG_COMPAT_MMT_REBORN) != 0;
}
// All local modules should have an author
// set to "Unknown" if author is missing.
if (local && moduleInfo.author == null) {
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!");
}
}
public static String readModulePropSimple(InputStream inputStream, String what) {
if (inputStream == null) return null;
String moduleId = null;
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
while (line.startsWith("\u0000"))
line = line.substring(1);
if (line.startsWith(what + "=")) {
moduleId = line.substring(what.length() + 1).trim();
@JvmStatic
fun readModulePropSimple(inputStream: InputStream?, what: String): String? {
if (inputStream == null) return null
var moduleId: String? = null
try {
BufferedReader(
InputStreamReader(inputStream, StandardCharsets.UTF_8)
).use { bufferedReader ->
var line: String
while (bufferedReader.readLine().also { line = it } != null) {
while (line.startsWith("\u0000")) line = line.substring(1)
if (line.startsWith("$what=")) {
moduleId = line.substring(what.length + 1).trim { it <= ' ' }
}
}
}
} catch (e: IOException) {
Timber.i(e)
}
} catch (IOException e) {
Timber.i(e);
return moduleId
}
return moduleId;
}
public static String readModuleId(InputStream inputStream) {
return readModulePropSimple(inputStream, "id");
}
public static void applyFallbacks(ModuleInfo moduleInfo) {
if (moduleInfo.support == null || moduleInfo.support.isEmpty()) {
moduleInfo.support = moduleSupportsFallbacks.get(moduleInfo.id);
}
if (moduleInfo.config == null || moduleInfo.config.isEmpty()) {
moduleInfo.config = moduleConfigsFallbacks.get(moduleInfo.id);
fun readModuleId(inputStream: InputStream?): String? {
return readModulePropSimple(inputStream, "id")
}
if (moduleInfo.minApi == 0) {
Integer minApiFallback = moduleMinApiFallbacks.get(moduleInfo.id);
if (minApiFallback != null)
moduleInfo.minApi = minApiFallback;
else if (moduleInfo.id.startsWith("riru_")
|| moduleInfo.id.startsWith("riru-"))
moduleInfo.minApi = RIRU_MIN_API;
@JvmStatic
fun applyFallbacks(moduleInfo: ModuleInfo) {
if (moduleInfo.support == null || moduleInfo.support!!.isEmpty()) {
moduleInfo.support = moduleSupportsFallbacks[moduleInfo.id]
}
if (moduleInfo.config == null || moduleInfo.config!!.isEmpty()) {
moduleInfo.config = moduleConfigsFallbacks[moduleInfo.id]
}
if (moduleInfo.minApi == 0) {
val minApiFallback = moduleMinApiFallbacks[moduleInfo.id]
if (minApiFallback != null) moduleInfo.minApi =
minApiFallback else if (moduleInfo.id.startsWith("riru_")
|| moduleInfo.id.startsWith("riru-")
) moduleInfo.minApi = RIRU_MIN_API
}
}
}
// Some module are really so low quality that it has become very annoying.
public static boolean isLowQualityModule(ModuleInfo moduleInfo) {
final String description;
return moduleInfo == null || moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)
|| moduleInfo.name.length() < 3 || moduleInfo.versionCode < 0
|| moduleInfo.author == null || !TextUtils.isGraphic(moduleInfo.author)
|| isNullString(description = moduleInfo.description) || !TextUtils.isGraphic(description)
|| description.toLowerCase(Locale.ROOT).equals(moduleInfo.name.toLowerCase(Locale.ROOT))
|| (getFlagsForModule(moduleInfo.id) & FLAG_COMPAT_LOW_QUALITY) != 0
|| (moduleInfo.id.startsWith("."));
}
// Some module are really so low quality that it has become very annoying.
@JvmStatic
fun isLowQualityModule(moduleInfo: ModuleInfo?): Boolean {
var description: String = moduleInfo?.description ?: return true
return (moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID) || moduleInfo.name!!.length < 3 || moduleInfo.versionCode < 0 || moduleInfo.author == null || !TextUtils.isGraphic(
moduleInfo.author
) || isNullString(moduleInfo.description.also {
description = it!!
}) || !TextUtils.isGraphic(description)) || description.lowercase() == moduleInfo.name!!.lowercase() || AppUpdateManager.getFlagsForModule(
moduleInfo.id
) and AppUpdateManager.FLAG_COMPAT_LOW_QUALITY != 0 || moduleInfo.id.startsWith(".")
}
private static boolean isInvalidValue(String name) {
return !TextUtils.isGraphic(name) || name.indexOf('\0') != -1;
}
private fun isInvalidValue(name: String?): Boolean {
return !TextUtils.isGraphic(name) || name!!.indexOf('\u0000') != -1
}
public 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;
}
@JvmStatic
fun isInvalidURL(url: String): Boolean {
val i = url.indexOf('/', 8)
val e = url.indexOf('.', 8)
return i == -1 || e == -1 || e >= i || !url.startsWith("https://") || url.length <= 12 || url.indexOf(
'\u0000'
) != -1
}
public static String makeNameFromId(String moduleId) {
return moduleId.substring(0, 1).toUpperCase(Locale.ROOT) +
moduleId.substring(1).replace('_', ' ');
}
private fun makeNameFromId(moduleId: String): String {
return moduleId.substring(0, 1).uppercase() +
moduleId.substring(1).replace('_', ' ')
}
public static boolean isNullString(String string) {
return string == null || string.isEmpty() || "null".equals(string);
}
@JvmStatic
fun isNullString(string: String?): Boolean {
return string.isNullOrEmpty() || "null" == string
}
// Make versionName no longer than 16 charters to avoid UI overflow.
public static String shortenVersionName(String versionName, long versionCode) {
if (isNullString(versionName)) return "v" + versionCode;
if (versionName.length() <= 16) return versionName;
int i = versionName.lastIndexOf('.');
if (i != -1 && i <= 16 && versionName.indexOf('.') != i
&& versionName.indexOf(' ') == -1)
return versionName.substring(0, i);
return "v" + versionCode;
// Make versionName no longer than 16 charters to avoid UI overflow.
fun shortenVersionName(versionName: String?, versionCode: Long): String {
if (isNullString(versionName)) return "v$versionCode"
if (versionName!!.length <= 16) return versionName
val i = versionName.lastIndexOf('.')
return if (i != -1 && i <= 16 && versionName.indexOf('.') != i && versionName.indexOf(
' '
) == -1
) versionName.substring(0, i) else "v$versionCode"
}
}
}
}

@ -121,7 +121,7 @@ public class SentryMain {
String url = (String) breadcrumb.getData("url");
if (url == null || url.isEmpty()) return breadcrumb;
if ("cloudflare-dns.com".equals(Uri.parse(url).getHost())) return null;
if (AndroidacyUtil.isAndroidacyLink(url)) {
if (AndroidacyUtil.Companion.isAndroidacyLink(url)) {
breadcrumb.setData("url", AndroidacyUtil.hideToken(url));
}
return breadcrumb;

@ -428,4 +428,5 @@
<string name="upgrade_snackbar">Androidacy Premium offers faster downloads, an ad-free experience, and more!</string>
<string name="upgrade_now">Upgrade</string>
<string name="crash_details_suggestion">The stacktrace may be found below. However, we <b>strongly</b> recommend you to use the feedback form below to submit feedback instead. This way, instead of manually copying the stacktrace, it will send it to us automatically. It also is deobfuscated that way and additional details are reported automatically.</string>
<string name="reinstall_warning">Most modules should be uninstalled, then reinstalled cleanly. For this reason, FoxMMM does not support reinstalling modules while they are installed on the device.</string>
</resources>

Loading…
Cancel
Save