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 if (downloadMode || backOnResume) return true
// sanitize url // sanitize url
@Suppress("NAME_SHADOWING") var url = request.url.toString() @Suppress("NAME_SHADOWING") var url = request.url.toString()
url = AndroidacyUtil.hideToken(url).toString() url = AndroidacyUtil.hideToken(url)
Timber.i("Exiting WebView %s", url) Timber.i("Exiting WebView %s", url)
IntentHelper.openUri(view.context, request.url.toString()) IntentHelper.openUri(view.context, request.url.toString())
return true return true

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

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

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

@ -2,6 +2,7 @@ package com.fox2code.mmm.module;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.text.Spanned; import android.text.Spanned;
import android.widget.Button; import android.widget.Button;
@ -52,7 +53,7 @@ public enum ActionButtonType {
} }
TrackHelper.track().event("view_notes", name).with(MainApplication.getINSTANCE().getTracker()); TrackHelper.track().event("view_notes", name).with(MainApplication.getINSTANCE().getTracker());
String notesUrl = moduleHolder.repoModule.notesUrl; 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()); IntentHelper.openUrlAndroidacy(button.getContext(), notesUrl, false, moduleHolder.repoModule.moduleInfo.name, moduleHolder.getMainModuleConfig());
} else { } 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); 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; name = moduleHolder.repoModule.moduleInfo.name;
} }
TrackHelper.track().event("view_update_install", name).with(MainApplication.getINSTANCE().getTracker()); 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(); String updateZipUrl = moduleHolder.getUpdateZipUrl();
if (updateZipUrl == null) return; if (updateZipUrl == null) return;
// Androidacy manage the selection between download and install // 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); IntentHelper.openUrlAndroidacy(button.getContext(), updateZipUrl, true, moduleInfo.name, moduleInfo.config);
return; return;
} }
@ -211,7 +230,7 @@ public enum ActionButtonType {
name = moduleHolder.repoModule.moduleInfo.name; name = moduleHolder.repoModule.moduleInfo.name;
} }
TrackHelper.track().event("config_module", name).with(MainApplication.getINSTANCE().getTracker()); 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); IntentHelper.openUrlAndroidacy(button.getContext(), config, true);
} else { } else {
IntentHelper.openConfig(button.getContext(), config); IntentHelper.openConfig(button.getContext(), config);

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

@ -121,7 +121,7 @@ public class SentryMain {
String url = (String) breadcrumb.getData("url"); String url = (String) breadcrumb.getData("url");
if (url == null || url.isEmpty()) return breadcrumb; if (url == null || url.isEmpty()) return breadcrumb;
if ("cloudflare-dns.com".equals(Uri.parse(url).getHost())) return null; 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)); breadcrumb.setData("url", AndroidacyUtil.hideToken(url));
} }
return breadcrumb; 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_snackbar">Androidacy Premium offers faster downloads, an ad-free experience, and more!</string>
<string name="upgrade_now">Upgrade</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="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> </resources>

Loading…
Cancel
Save