conversion or something

idk, im not religious

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/89/head
androidacy-user 2 years ago
parent 19174c3b81
commit 7dc626328f

@ -387,7 +387,7 @@ androidComponents {
// Assigns the new version code to output.versionCode, which changes the version code
// for only the output APK, not for the variant itself.
val versioCode = output.versionCode.get() as Int
output.versionCode.set(baseAbiCode * 1000 + versioCode)
output.versionCode.set((baseAbiCode * 1000) + versioCode)
}
}
}
@ -438,7 +438,7 @@ dependencies {
// implementation("com.google.protobuf:protobuf-javalite:3.22.2")
// google guava, maybe fix a bug
implementation("com.google.guava:guava:32.0.0-jre")
implementation("com.google.guava:guava:32.0.1-jre")
val libsuVersion = "5.0.5"
@ -455,12 +455,12 @@ dependencies {
implementation("com.github.Fox2Code:AndroidANSI:1.2.1")
// sentry
implementation("io.sentry:sentry-android:6.21.0")
implementation("io.sentry:sentry-android-timber:6.21.0")
implementation("io.sentry:sentry-android-fragment:6.21.0")
implementation("io.sentry:sentry-android-okhttp:6.21.0")
implementation("io.sentry:sentry-kotlin-extensions:6.21.0")
implementation("io.sentry:sentry-android-ndk:6.21.0")
implementation("io.sentry:sentry-android:6.22.0")
implementation("io.sentry:sentry-android-timber:6.22.0")
implementation("io.sentry:sentry-android-fragment:6.22.0")
implementation("io.sentry:sentry-android-okhttp:6.22.0")
implementation("io.sentry:sentry-kotlin-extensions:6.22.0")
implementation("io.sentry:sentry-android-ndk:6.22.0")
// Markdown
// TODO: switch to an updated implementation

@ -48,9 +48,9 @@ class AppUpdateManager private constructor() {
if (lastChecked != this.lastChecked) return peekShouldUpdate()
// make a request to https://production-api.androidacy.com/amm/updates/check with appVersionCode and token/device_id/client_id
var token = AndroidacyRepoData.token
if (!AndroidacyRepoData.getInstance().isValidToken(token)) {
if (!AndroidacyRepoData.instance.isValidToken(token)) {
Timber.w("Invalid token, not checking for updates")
token = AndroidacyRepoData.getInstance().requestNewToken()
token = AndroidacyRepoData.instance.requestNewToken()
}
val deviceId = AndroidacyRepoData.generateDeviceId()
val clientId = BuildConfig.ANDROIDACY_CLIENT_ID

@ -346,7 +346,7 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis
NotificationType.NO_INTERNET.autoAdd(moduleViewListBuilderOnline)
val progressIndicator = progressIndicator!!
// hide progress bar is repo-manager says we have no internet
if (!RepoManager.getINSTANCE().hasConnectivity()) {
if (!RepoManager.getINSTANCE()!!.hasConnectivity()) {
Timber.i("No connection, hiding progress")
runOnUiThread {
progressIndicator.visibility = View.GONE
@ -377,9 +377,9 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis
Timber.i("Scanning for modules!")
if (BuildConfig.DEBUG) Timber.i("Initialize Update")
val max = instance!!.getUpdatableModuleCount()
if (RepoManager.getINSTANCE().customRepoManager != null && RepoManager.getINSTANCE().customRepoManager.needUpdate()) {
if (RepoManager.getINSTANCE()!!.customRepoManager != null && RepoManager.getINSTANCE()!!.customRepoManager!!.needUpdate()) {
Timber.w("Need update on create")
} else if (RepoManager.getINSTANCE().customRepoManager == null) {
} else if (RepoManager.getINSTANCE()!!.customRepoManager == null) {
Timber.w("CustomRepoManager is null")
}
// update compat metadata
@ -388,7 +388,7 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis
if (BuildConfig.DEBUG) Timber.i("Check Update")
// update repos
if (hasWebView()) {
RepoManager.getINSTANCE().update { value: Double ->
RepoManager.getINSTANCE()!!.update { value: Double ->
runOnUiThread(if (max == 0) Runnable {
progressIndicator.setProgressCompat(
(value * PRECISION).toInt(), true
@ -452,7 +452,7 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis
}
if (BuildConfig.DEBUG) Timber.i("Apply")
RepoManager.getINSTANCE()
.runAfterUpdate { moduleViewListBuilderOnline.appendRemoteModules() }
?.runAfterUpdate { moduleViewListBuilderOnline.appendRemoteModules() }
moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter!!)
moduleViewListBuilder.applyTo(moduleListOnline, moduleViewAdapterOnline!!)
moduleViewListBuilderOnline.applyTo(moduleListOnline, moduleViewAdapterOnline!!)
@ -599,14 +599,14 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis
if (appUpdateManager.checkUpdate(false)) moduleViewListBuilder.addNotification(
NotificationType.UPDATE_AVAILABLE
)
RepoManager.getINSTANCE().updateEnabledStates()
if (RepoManager.getINSTANCE().customRepoManager.needUpdate()) {
RepoManager.getINSTANCE()!!.updateEnabledStates()
if (RepoManager.getINSTANCE()!!.customRepoManager!!.needUpdate()) {
runOnUiThread {
progressIndicator!!.isIndeterminate = false
progressIndicator!!.max = PRECISION
}
if (BuildConfig.DEBUG) Timber.i("Check Update")
RepoManager.getINSTANCE().update { value: Double ->
RepoManager.getINSTANCE()!!.update { value: Double ->
runOnUiThread {
progressIndicator!!.setProgressCompat(
(value * PRECISION).toInt(), true
@ -620,7 +620,7 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis
}
if (BuildConfig.DEBUG) Timber.i("Apply")
RepoManager.getINSTANCE()
.runAfterUpdate { moduleViewListBuilderOnline.appendRemoteModules() }
?.runAfterUpdate { moduleViewListBuilderOnline.appendRemoteModules() }
Timber.i("Common Before applyTo")
moduleViewListBuilder.applyTo(moduleList!!, moduleViewAdapter!!)
moduleViewListBuilderOnline.applyTo(moduleListOnline!!, moduleViewAdapterOnline!!)
@ -647,7 +647,7 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis
Thread({
cleanDnsCache() // Allow DNS reload from network
val max = instance!!.getUpdatableModuleCount()
RepoManager.getINSTANCE().update { value: Double ->
RepoManager.getINSTANCE()!!.update { value: Double ->
runOnUiThread(if (max == 0) Runnable {
progressIndicator!!.setProgressCompat(
(value * PRECISION).toInt(), true
@ -699,11 +699,11 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis
swipeRefreshLayout!!.isRefreshing = false
}
NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder)
RepoManager.getINSTANCE().updateEnabledStates()
RepoManager.getINSTANCE()!!.updateEnabledStates()
RepoManager.getINSTANCE()
.runAfterUpdate { moduleViewListBuilder.appendInstalledModules() }
?.runAfterUpdate { moduleViewListBuilder.appendInstalledModules() }
RepoManager.getINSTANCE()
.runAfterUpdate { moduleViewListBuilderOnline.appendRemoteModules() }
?.runAfterUpdate { moduleViewListBuilderOnline.appendRemoteModules() }
moduleViewListBuilder.applyTo(moduleList!!, moduleViewAdapter!!)
moduleViewListBuilderOnline.applyTo(moduleListOnline!!, moduleViewAdapterOnline!!)
}, "Repo update thread").start()
@ -784,21 +784,13 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis
}
fun maybeShowUpgrade() {
if (AndroidacyRepoData.getInstance() == null || AndroidacyRepoData.getInstance().memberLevel == null) {
if (AndroidacyRepoData.instance.memberLevel == null) {
// wait for up to 10 seconds for AndroidacyRepoData to be initialized
var i = 0
while (AndroidacyRepoData.getInstance() == null && i < 10) {
try {
Thread.sleep(1000)
} catch (e: InterruptedException) {
Timber.e(e)
}
i++
}
if (AndroidacyRepoData.getInstance().isEnabled && AndroidacyRepoData.getInstance().memberLevel == null) {
var i: Int
if (AndroidacyRepoData.instance.isEnabled && AndroidacyRepoData.instance.memberLevel == null) {
Timber.d("Member level is null, waiting for it to be initialized")
i = 0
while (AndroidacyRepoData.getInstance().memberLevel == null && i < 20) {
while (AndroidacyRepoData.instance.memberLevel == null && i < 20) {
i++
try {
Thread.sleep(500)
@ -808,32 +800,32 @@ class MainActivity : FoxActivity(), OnRefreshListener, SearchView.OnQueryTextLis
}
}
// if it's still null, but it's enabled, throw an error
if (AndroidacyRepoData.getInstance().isEnabled && AndroidacyRepoData.getInstance().memberLevel == null) {
if (AndroidacyRepoData.instance.isEnabled && AndroidacyRepoData.instance.memberLevel == null) {
Timber.e("AndroidacyRepoData is enabled, but member level is null")
}
if (AndroidacyRepoData.getInstance() != null && AndroidacyRepoData.getInstance().isEnabled && AndroidacyRepoData.getInstance().memberLevel == "Guest") {
if (AndroidacyRepoData.instance.isEnabled && AndroidacyRepoData.instance.memberLevel == "Guest") {
runtimeUtils!!.showUpgradeSnackbar(this, this)
} else {
if (!AndroidacyRepoData.getInstance().isEnabled) {
if (!AndroidacyRepoData.instance.isEnabled) {
Timber.i("AndroidacyRepoData is disabled, not showing upgrade snackbar 1")
} else if (AndroidacyRepoData.getInstance().memberLevel != "Guest") {
} else if (AndroidacyRepoData.instance.memberLevel != "Guest") {
Timber.i(
"AndroidacyRepoData is not Guest, not showing upgrade snackbar 1. Level: %s",
AndroidacyRepoData.getInstance().memberLevel
AndroidacyRepoData.instance.memberLevel
)
} else {
Timber.i("Unknown error, not showing upgrade snackbar 1")
}
}
} else if (AndroidacyRepoData.getInstance().isEnabled && AndroidacyRepoData.getInstance().memberLevel == "Guest") {
} else if (AndroidacyRepoData.instance.isEnabled && AndroidacyRepoData.instance.memberLevel == "Guest") {
runtimeUtils!!.showUpgradeSnackbar(this, this)
} else {
if (!AndroidacyRepoData.getInstance().isEnabled) {
if (!AndroidacyRepoData.instance.isEnabled) {
Timber.i("AndroidacyRepoData is disabled, not showing upgrade snackbar 2")
} else if (AndroidacyRepoData.getInstance().memberLevel != "Guest") {
} else if (AndroidacyRepoData.instance.memberLevel != "Guest") {
Timber.i(
"AndroidacyRepoData is not Guest, not showing upgrade snackbar 2. Level: %s",
AndroidacyRepoData.getInstance().memberLevel
AndroidacyRepoData.instance.memberLevel
)
} else {
Timber.i("Unknown error, not showing upgrade snackbar 2")

@ -104,13 +104,13 @@ enum class NotificationType constructor(
@JvmStatic
NO_INTERNET(R.string.fail_internet, R.drawable.ic_baseline_cloud_off_24) {
override fun shouldRemove(): Boolean {
return RepoManager.getINSTANCE().hasConnectivity()
return RepoManager.getINSTANCE()!!.hasConnectivity()
}
},
@JvmStatic
REPO_UPDATE_FAILED(R.string.repo_update_failed, R.drawable.ic_baseline_cloud_off_24) {
override fun shouldRemove(): Boolean {
return RepoManager.getINSTANCE().isLastUpdateSuccess
return RepoManager.getINSTANCE()!!.isLastUpdateSuccess
}
},
@JvmStatic
@ -124,7 +124,7 @@ enum class NotificationType constructor(
)
}) {
override fun shouldRemove(): Boolean {
return (!RepoManager.isAndroidacyRepoEnabled()
return (!RepoManager.isAndroidacyRepoEnabled
|| !Http.needCaptchaAndroidacy())
}
},

@ -229,9 +229,9 @@ class UpdateActivity : FoxActivity() {
// check for update
val shouldUpdate = AppUpdateManager.appUpdateManager.checkUpdate(true)
var token = AndroidacyRepoData.token
if (!AndroidacyRepoData.getInstance().isValidToken(token)) {
if (!AndroidacyRepoData.instance.isValidToken(token)) {
Timber.w("Invalid token, not checking for updates")
token = AndroidacyRepoData.getInstance().requestNewToken()
token = AndroidacyRepoData.instance.requestNewToken()
}
val deviceId = AndroidacyRepoData.generateDeviceId()
val clientId = BuildConfig.ANDROIDACY_CLIENT_ID
@ -271,9 +271,9 @@ class UpdateActivity : FoxActivity() {
// get status text view
val statusTextView = findViewById<MaterialTextView>(R.id.update_progress_text)
var token = AndroidacyRepoData.token
if (!AndroidacyRepoData.getInstance().isValidToken(token)) {
if (!AndroidacyRepoData.instance.isValidToken(token)) {
Timber.w("Invalid token, not checking for updates")
token = AndroidacyRepoData.getInstance().requestNewToken()
token = AndroidacyRepoData.instance.requestNewToken()
}
val deviceId = AndroidacyRepoData.generateDeviceId()
val clientId = BuildConfig.ANDROIDACY_CLIENT_ID

@ -65,16 +65,16 @@ enum class XHooks {
@Keep
fun addXRepo(url: String?, fallbackName: String?): XRepo {
return RepoManager.getINSTANCE_UNSAFE().addOrGet(url, fallbackName)
return url?.let { RepoManager.iNSTANCE_UNSAFE?.addOrGet(it, fallbackName) }!!
}
@Keep
fun getXRepo(url: String?): XRepo {
return RepoManager.getINSTANCE_UNSAFE()[url]
return RepoManager.iNSTANCE_UNSAFE?.get(url) ?: throw NullPointerException("Repo not found!")
}
@get:Keep
val xRepos: Collection<XRepo>
get() = RepoManager.getINSTANCE_UNSAFE().xRepos
get() = RepoManager.iNSTANCE_UNSAFE!!.xRepos
}
}

@ -1,482 +0,0 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.androidacy;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.fingerprintjs.android.fingerprint.Fingerprinter;
import com.fingerprintjs.android.fingerprint.FingerprinterFactory;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.repo.RepoData;
import com.fox2code.mmm.repo.RepoManager;
import com.fox2code.mmm.repo.RepoModule;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.io.net.HttpException;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import okhttp3.HttpUrl;
import timber.log.Timber;
@SuppressWarnings("KotlinInternalInJava")
public final class AndroidacyRepoData extends RepoData {
public static String ANDROIDACY_DEVICE_ID = null;
public static String token = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).getString("pref_androidacy_api_token", null);
static {
HttpUrl.Builder OK_HTTP_URL_BUILDER = new HttpUrl.Builder().scheme("https");
// Using HttpUrl.Builder.host(String) crash the app
OK_HTTP_URL_BUILDER.setHost$okhttp(".androidacy.com");
OK_HTTP_URL_BUILDER.build();
}
@SuppressWarnings("unused")
public final String ClientID = BuildConfig.ANDROIDACY_CLIENT_ID;
private final boolean testMode;
private final String host;
public String[][] userInfo = new String[][]{{"role", null}, {"permissions", null}};
public String memberLevel;
// Avoid spamming requests to Androidacy
private long androidacyBlockade = 0;
public AndroidacyRepoData(File cacheRoot, boolean testMode) {
super(testMode ? RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT : RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT, cacheRoot);
this.defaultName = "Androidacy Modules Repo";
this.defaultWebsite = RepoManager.ANDROIDACY_MAGISK_REPO_HOMEPAGE;
this.defaultSupport = "https://t.me/androidacy_discussions";
this.defaultDonate = "https://www.androidacy.com/membership-account/membership-checkout/?level=2&discount_code=FOX2CODE&utm_souce=foxmmm&utm_medium=android-app&utm_campaign=fox-upgrade-promo";
this.defaultSubmitModule = "https://www.androidacy.com/module-repository-applications/";
this.host = testMode ? "staging-api.androidacy.com" : "production-api.androidacy.com";
this.testMode = testMode;
}
public static AndroidacyRepoData getInstance() {
return RepoManager.getINSTANCE().getAndroidacyRepoData();
}
private static String filterURL(String url) {
if (url == null || url.isEmpty() || PropUtils.isInvalidURL(url)) {
return null;
}
return url;
}
// Generates a unique device ID. This is used to identify the device in the API for rate
// limiting and fraud detection.
public static String generateDeviceId() {
// first, check if ANDROIDACY_DEVICE_ID is already set
if (ANDROIDACY_DEVICE_ID != null) {
return ANDROIDACY_DEVICE_ID;
}
// Try to get the device ID from the shared preferences
SharedPreferences sharedPreferences = MainApplication.getSharedPreferences("androidacy");
String deviceIdPref = Objects.requireNonNull(sharedPreferences).getString("device_id_v2", null);
if (deviceIdPref != null) {
ANDROIDACY_DEVICE_ID = deviceIdPref;
return deviceIdPref;
} else {
Fingerprinter fp = FingerprinterFactory.create(Objects.requireNonNull(MainApplication.getINSTANCE()).getApplicationContext());
fp.getFingerprint(Fingerprinter.Version.V_5, fingerprint -> {
ANDROIDACY_DEVICE_ID = fingerprint;
// use fingerprint
// Save the device ID to the shared preferences
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("device_id_v2", ANDROIDACY_DEVICE_ID);
editor.apply();
return null;
});
// wait for up to 5 seconds for the fingerprint to be generated (ANDROIDACY_DEVICE_ID to be set)
long startTime = System.currentTimeMillis();
while (ANDROIDACY_DEVICE_ID == null && System.currentTimeMillis() - startTime < 5000) {
try {
//noinspection BusyWait
Thread.sleep(100);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
if (ANDROIDACY_DEVICE_ID == null) {
// fingerprint generation failed, use a random UUID
ANDROIDACY_DEVICE_ID = UUID.randomUUID().toString();
}
return ANDROIDACY_DEVICE_ID;
}
}
public boolean isValidToken(String token) throws IOException {
String deviceId = generateDeviceId();
try {
byte[] resp = Http.doHttpGet("https://" + this.host + "/auth/me?token=" + token + "&device_id=" + deviceId + "&client_id=" + BuildConfig.ANDROIDACY_CLIENT_ID, false);
// response is JSON
JSONObject jsonObject = new JSONObject(new String(resp));
memberLevel = jsonObject.getString("role");
Timber.d("Member level: %s", memberLevel);
JSONArray memberPermissions = jsonObject.getJSONArray("permissions");
// set role and permissions on userInfo property
userInfo = new String[][]{{"role", memberLevel}, {"permissions", String.valueOf(memberPermissions)}};
return true;
} catch (HttpException e) {
if (e.getErrorCode() == 401) {
Timber.w("Invalid token, resetting...");
// Remove saved preference
SharedPreferences.Editor editor = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).edit();
editor.remove("pref_androidacy_api_token");
editor.apply();
return false;
}
throw e;
} catch (JSONException e) {
// response is not JSON
Timber.w("Invalid token, resetting...");
Timber.w(e);
// Remove saved preference
SharedPreferences.Editor editor = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).edit();
editor.remove("pref_androidacy_api_token");
editor.apply();
return false;
}
}
/**
* Request a new token from the server and save it to the shared preferences
* @return String token
*/
public String requestNewToken() throws IOException, JSONException {
String deviceId = generateDeviceId();
byte[] resp = Http.doHttpGet("https://" + this.host + "/auth/register?device_id=" + deviceId + "&client_id=" + BuildConfig.ANDROIDACY_CLIENT_ID, false);
// response is JSON
JSONObject jsonObject = new JSONObject(new String(resp));
String token = jsonObject.getString("token");
// Save the token to the shared preferences
SharedPreferences.Editor editor = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).edit();
editor.putString("pref_androidacy_api_token", token);
editor.apply();
return token;
}
@SuppressLint({"RestrictedApi", "BinaryOperationInTimber"})
@Override
protected boolean prepare() {
// If ANDROIDACY_CLIENT_ID is not set or is empty, disable this repo and return
if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) {
SharedPreferences.Editor editor = Objects.requireNonNull(MainApplication.getSharedPreferences("mmm")).edit();
editor.putBoolean("pref_androidacy_repo_enabled", false);
editor.apply();
Timber.w("ANDROIDACY_CLIENT_ID is empty, disabling AndroidacyRepoData 2");
return false;
}
if (Http.needCaptchaAndroidacy()) return false;
// Implementation details discussed on telegram
// First, ping the server to check if it's alive
try {
HttpURLConnection connection = (HttpURLConnection) new URL("https://" + this.host + "/ping").openConnection();
connection.setRequestMethod("GET");
connection.setReadTimeout(5000);
connection.connect();
if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) {
// If it's a 400, the app is probably outdated. Show a snackbar suggesting user update app and webview
if (connection.getResponseCode() == 400) {
// Show a dialog using androidacy_update_needed string
new MaterialAlertDialogBuilder(Objects.requireNonNull(MainApplication.getINSTANCE())).setTitle(R.string.androidacy_update_needed).setMessage(R.string.androidacy_update_needed_message).setPositiveButton(R.string.update, (dialog, which) -> {
// Open the app's page on the Play Store
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://www.androidacy.com/downloads/?view=FoxMMM&utm_source=foxmnm&utm_medium=app&utm_campaign=android-app"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
MainApplication.getINSTANCE().startActivity(intent);
}).setNegativeButton(R.string.cancel, null).show();
}
return false;
}
} catch (Exception e) {
Timber.e(e, "Failed to ping server");
return false;
}
long time = System.currentTimeMillis();
if (this.androidacyBlockade > time) return true; // fake it till you make it. Basically,
// don't fail just because we're rate limited. API and web rate limits are different.
this.androidacyBlockade = time + 30_000L;
try {
if (token == null) {
token = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).getString("pref_androidacy_api_token", null);
if (token != null && !this.isValidToken(token)) {
Timber.i("Token expired or invalid, requesting new one...");
token = null;
} else {
Timber.i("Using cached token");
}
} else if (!this.isValidToken(token)) {
Timber.i("Token expired, requesting new one...");
token = null;
} else {
Timber.i("Using validated cached token");
}
} catch (IOException e) {
if (HttpException.shouldTimeout(e)) {
Timber.e(e, "We are being rate limited!");
this.androidacyBlockade = time + 3_600_000L;
}
return false;
}
if (token == null) {
Timber.i("Token is null, requesting new one...");
try {
Timber.i("Requesting new token...");
// POST json request to https://production-api.androidacy.com/auth/register
token = requestNewToken();
// Parse token
try {
JSONObject jsonObject = new JSONObject(token);
// log last four of token, replacing the rest with asterisks
token = jsonObject.getString("token");
//noinspection SuspiciousRegexArgument
Timber.d("Token: %s", token.substring(0, token.length() - 4).replaceAll(".", "*") + token.substring(token.length() - 4));
memberLevel = jsonObject.getString("role");
Timber.d("Member level: %s", memberLevel);
} catch (JSONException e) {
Timber.e(e, "Failed to parse token: %s", token);
// Show a toast
Looper mainLooper = Looper.getMainLooper();
Handler handler = new Handler(mainLooper);
handler.post(() -> Toast.makeText(MainApplication.getINSTANCE(), R.string.androidacy_failed_to_parse_token, Toast.LENGTH_LONG).show());
return false;
}
// Ensure token is valid
if (!isValidToken(token)) {
Timber.e("Failed to validate token");
// Show a toast
Looper mainLooper = Looper.getMainLooper();
Handler handler = new Handler(mainLooper);
handler.post(() -> Toast.makeText(MainApplication.getINSTANCE(), R.string.androidacy_failed_to_validate_token, Toast.LENGTH_LONG).show());
return false;
} else {
// Save token to shared preference
SharedPreferences.Editor editor = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).edit();
editor.putString("pref_androidacy_api_token", token);
editor.apply();
Timber.i("Token saved to shared preference");
}
} catch (Exception e) {
if (HttpException.shouldTimeout(e)) {
Timber.e(e, "We are being rate limited!");
this.androidacyBlockade = time + 3_600_000L;
}
Timber.e(e, "Failed to get a new token");
return false;
}
}
return true;
}
@Override
protected List<RepoModule> populate(JSONObject jsonObject) throws JSONException {
Timber.d("AndroidacyRepoData populate start");
String name = jsonObject.optString("name", "Androidacy Modules Repo");
String nameForModules = name.endsWith(" (Official)") ? name.substring(0, name.length() - 11) : name;
JSONArray jsonArray;
try {
jsonArray = jsonObject.getJSONArray("data");
} catch (JSONException e) {
// probably using modules key since it's cached
try {
jsonArray = jsonObject.getJSONArray("modules");
} catch (JSONException e2) {
// we should never get here, bail out
Timber.e(e2, "Failed to parse modules");
return null;
}
}
for (RepoModule repoModule : this.moduleHashMap.values()) {
repoModule.processed = false;
}
ArrayList<RepoModule> newModules = new ArrayList<>();
int len = jsonArray.length();
long lastLastUpdate = 0;
for (int i = 0; i < len; i++) {
jsonObject = jsonArray.getJSONObject(i);
String moduleId;
try {
moduleId = jsonObject.getString("codename");
} catch (JSONException e) {
Timber.e("Module %s has no codename or json %s is invalid", jsonObject.optString("codename", "Unknown"), jsonObject.toString());
continue;
}
// Normally, we'd validate the module id here, but we don't need to because the server does it for us
long lastUpdate;
try {
lastUpdate = jsonObject.getLong("updated_at") * 1000;
} catch (JSONException e) {
lastUpdate = jsonObject.getLong("lastUpdate") * 1000;
}
lastLastUpdate = Math.max(lastLastUpdate, lastUpdate);
RepoModule repoModule = this.moduleHashMap.get(moduleId);
if (repoModule == null) {
repoModule = new RepoModule(this, moduleId);
repoModule.moduleInfo.flags = 0;
this.moduleHashMap.put(moduleId, repoModule);
newModules.add(repoModule);
} else {
if (repoModule.lastUpdated < lastUpdate) {
newModules.add(repoModule);
}
}
repoModule.processed = true;
repoModule.lastUpdated = lastUpdate;
repoModule.repoName = nameForModules;
repoModule.zipUrl = filterURL(jsonObject.optString("zipUrl", ""));
repoModule.notesUrl = filterURL(jsonObject.optString("notesUrl", ""));
if (repoModule.zipUrl == null) {
repoModule.zipUrl = // Fallback url in case the API doesn't have zipUrl
"https://" + this.host + "/magisk/info/" + moduleId;
}
if (repoModule.notesUrl == null) {
repoModule.notesUrl = // Fallback url in case the API doesn't have notesUrl
"https://" + this.host + "/magisk/readme/" + moduleId;
}
repoModule.zipUrl = this.injectToken(repoModule.zipUrl);
repoModule.notesUrl = this.injectToken(repoModule.notesUrl);
repoModule.qualityText = R.string.module_downloads;
repoModule.qualityValue = jsonObject.optInt("downloads", 0);
if (repoModule.qualityValue == 0) {
repoModule.qualityValue = jsonObject.optInt("stats", 0);
}
String checksum = jsonObject.optString("checksum", "");
repoModule.checksum = checksum.isEmpty() ? null : checksum;
ModuleInfo moduleInfo = repoModule.moduleInfo;
moduleInfo.name = jsonObject.getString("name");
moduleInfo.versionCode = jsonObject.getLong("versionCode");
moduleInfo.version = jsonObject.optString("version", "v" + moduleInfo.versionCode);
moduleInfo.author = jsonObject.optString("author", "Unknown");
moduleInfo.description = jsonObject.optString("description", "");
moduleInfo.minApi = jsonObject.getInt("minApi");
moduleInfo.maxApi = jsonObject.getInt("maxApi");
String minMagisk = jsonObject.getString("minMagisk");
try {
int c = minMagisk.indexOf('.');
if (c == -1) {
moduleInfo.minMagisk = Integer.parseInt(minMagisk);
} else {
moduleInfo.minMagisk = // Allow 24.1 to mean 24100
(Integer.parseInt(minMagisk.substring(0, c)) * 1000) + (Integer.parseInt(minMagisk.substring(c + 1)) * 100);
}
} catch (Exception e) {
moduleInfo.minMagisk = 0;
}
moduleInfo.needRamdisk = jsonObject.optBoolean("needRamdisk", false);
moduleInfo.changeBoot = jsonObject.optBoolean("changeBoot", false);
moduleInfo.mmtReborn = jsonObject.optBoolean("mmtReborn", false);
moduleInfo.support = filterURL(jsonObject.optString("support"));
moduleInfo.donate = filterURL(jsonObject.optString("donate"));
moduleInfo.safe = (jsonObject.has("vt_status") && jsonObject.getString("vt_status").equalsIgnoreCase("clean")) || jsonObject.optBoolean("safe", false);
String config = jsonObject.optString("config", "");
moduleInfo.config = config.isEmpty() ? null : config;
PropUtils.applyFallbacks(moduleInfo); // Apply fallbacks
}
Iterator<RepoModule> moduleInfoIterator = this.moduleHashMap.values().iterator();
while (moduleInfoIterator.hasNext()) {
RepoModule repoModule = moduleInfoIterator.next();
if (!repoModule.processed) {
moduleInfoIterator.remove();
} else {
repoModule.moduleInfo.verify();
}
}
this.lastUpdate = lastLastUpdate;
this.name = name;
this.website = jsonObject.optString("website");
this.support = jsonObject.optString("support");
this.donate = jsonObject.optString("donate");
this.submitModule = jsonObject.optString("submitModule");
return newModules;
}
@Override
public void storeMetadata(RepoModule repoModule, byte[] data) {
}
@Override
public boolean tryLoadMetadata(RepoModule repoModule) {
if (this.moduleHashMap.containsKey(repoModule.id)) {
repoModule.moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID;
return true;
}
repoModule.moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID;
return false;
}
@Override
public String getUrl() {
return token == null ? this.url : this.url + "?token=" + token + "&v=" + BuildConfig.VERSION_CODE + "&c=" + BuildConfig.VERSION_NAME + "&device_id=" + generateDeviceId() + "&client_id=" + BuildConfig.ANDROIDACY_CLIENT_ID;
}
private String injectToken(String url) {
// Do not inject token for non Androidacy urls
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));
url = "https://staging-api.androidacy.com/" + url.substring(38);
}
} else {
if (url.startsWith("https://staging-api.androidacy.com/")) {
Timber.e("Got test mode url: %s", AndroidacyUtil.hideToken(url));
url = "https://production-api.androidacy.com/" + url.substring(35);
}
}
String token = "token=" + AndroidacyRepoData.token;
String deviceId = "device_id=" + generateDeviceId();
if (!url.contains(token)) {
if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
return url + '&' + token;
} else {
return url + '?' + token;
}
}
if (!url.contains(deviceId)) {
if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
return url + '&' + deviceId;
} else {
return url + '?' + deviceId;
}
}
return url;
}
@NonNull
@Override
public String getName() {
return this.testMode ? super.getName() + " (Test Mode)" : super.getName();
}
public void setToken(String token) {
if (Http.hasWebView()) {
AndroidacyRepoData.token = token;
}
}
}

@ -0,0 +1,522 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.androidacy
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import com.fingerprintjs.android.fingerprint.Fingerprinter
import com.fingerprintjs.android.fingerprint.FingerprinterFactory.create
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.MainApplication.Companion.INSTANCE
import com.fox2code.mmm.MainApplication.Companion.getSharedPreferences
import com.fox2code.mmm.R
import com.fox2code.mmm.androidacy.AndroidacyUtil.Companion.hideToken
import com.fox2code.mmm.androidacy.AndroidacyUtil.Companion.isAndroidacyLink
import com.fox2code.mmm.manager.ModuleInfo
import com.fox2code.mmm.repo.RepoData
import com.fox2code.mmm.repo.RepoManager
import com.fox2code.mmm.repo.RepoModule
import com.fox2code.mmm.utils.io.PropUtils.Companion.applyFallbacks
import com.fox2code.mmm.utils.io.PropUtils.Companion.isInvalidURL
import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet
import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView
import com.fox2code.mmm.utils.io.net.Http.Companion.needCaptchaAndroidacy
import com.fox2code.mmm.utils.io.net.HttpException
import com.fox2code.mmm.utils.io.net.HttpException.Companion.shouldTimeout
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import okhttp3.HttpUrl.Builder
import okhttp3.HttpUrl.Builder.*
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.util.UUID
import kotlin.math.max
class AndroidacyRepoData(cacheRoot: File?, testMode: Boolean) : RepoData(
if (testMode) RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT else RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT,
cacheRoot!!
) {
private val clientID = BuildConfig.ANDROIDACY_CLIENT_ID
private val testMode: Boolean
private val host: String
@JvmField
var userInfo = arrayOf(arrayOf("role", null), arrayOf("permissions", null))
@JvmField
var memberLevel: String? = null
// Avoid spamming requests to Androidacy
private var androidacyBlockade: Long = 0
init {
defaultName = "Androidacy Modules Repo"
defaultWebsite = RepoManager.ANDROIDACY_MAGISK_REPO_HOMEPAGE
defaultSupport = "https://t.me/androidacy_discussions"
defaultDonate =
"https://www.androidacy.com/membership-account/membership-checkout/?level=2&discount_code=FOX2CODE&utm_souce=foxmmm&utm_medium=android-app&utm_campaign=fox-upgrade-promo"
defaultSubmitModule = "https://www.androidacy.com/module-repository-applications/"
host = if (testMode) "staging-api.androidacy.com" else "production-api.androidacy.com"
this.testMode = testMode
}
@Throws(IOException::class)
fun isValidToken(token: String?): Boolean {
val deviceId = generateDeviceId()
return try {
val resp = doHttpGet(
"https://$host/auth/me?token=$token&device_id=$deviceId&client_id=$clientID",
false
)
// response is JSON
val jsonObject = JSONObject(String(resp))
memberLevel = jsonObject.getString("role")
Timber.d("Member level: %s", memberLevel)
val memberPermissions = jsonObject.getJSONArray("permissions")
// set role and permissions on userInfo property
userInfo = arrayOf(
arrayOf("role", memberLevel),
arrayOf("permissions", memberPermissions.toString())
)
true
} catch (e: HttpException) {
if (e.errorCode == 401) {
Timber.w("Invalid token, resetting...")
// Remove saved preference
val editor = getSharedPreferences("androidacy")!!.edit()
editor.remove("pref_androidacy_api_token")
editor.apply()
return false
}
throw e
} catch (e: JSONException) {
// response is not JSON
Timber.w("Invalid token, resetting...")
Timber.w(e)
// Remove saved preference
val editor = getSharedPreferences("androidacy")!!.edit()
editor.remove("pref_androidacy_api_token")
editor.apply()
false
}
}
/**
* Request a new token from the server and save it to the shared preferences
* @return String token
*/
@Throws(IOException::class, JSONException::class)
fun requestNewToken(): String {
val deviceId = generateDeviceId()
val resp = doHttpGet(
"https://" + host + "/auth/register?device_id=" + deviceId + "&client_id=" + BuildConfig.ANDROIDACY_CLIENT_ID,
false
)
// response is JSON
val jsonObject = JSONObject(String(resp))
val token = jsonObject.getString("token")
// Save the token to the shared preferences
val editor = getSharedPreferences("androidacy")!!.edit()
editor.putString("pref_androidacy_api_token", token)
editor.apply()
return token
}
@SuppressLint("RestrictedApi", "BinaryOperationInTimber")
override fun prepare(): Boolean {
// If ANDROIDACY_CLIENT_ID is not set or is empty, disable this repo and return
@Suppress("KotlinConstantConditions")
if (BuildConfig.ANDROIDACY_CLIENT_ID == "") {
val editor = getSharedPreferences("mmm")!!.edit()
editor.putBoolean("pref_androidacy_repo_enabled", false)
editor.apply()
Timber.w("ANDROIDACY_CLIENT_ID is empty, disabling AndroidacyRepoData 2")
return false
}
if (needCaptchaAndroidacy()) return false
// Implementation details discussed on telegram
// First, ping the server to check if it's alive
try {
val connection = URL("https://$host/ping").openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.readTimeout = 5000
connection.connect()
if (connection.responseCode != 200 && connection.responseCode != 204) {
// If it's a 400, the app is probably outdated. Show a snackbar suggesting user update app and webview
if (connection.responseCode == 400) {
// Show a dialog using androidacy_update_needed string
INSTANCE?.let { MaterialAlertDialogBuilder(it) }!!.setTitle(R.string.androidacy_update_needed)
.setMessage(
R.string.androidacy_update_needed_message
)
.setPositiveButton(R.string.update) { _: DialogInterface?, _: Int ->
// Open the app's page on the Play Store
val intent = Intent(Intent.ACTION_VIEW)
intent.data =
Uri.parse("https://www.androidacy.com/downloads/?view=FoxMMM&utm_source=foxmnm&utm_medium=app&utm_campaign=android-app")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
INSTANCE!!.startActivity(intent)
}.setNegativeButton(R.string.cancel, null).show()
}
return false
}
} catch (e: Exception) {
Timber.e(e, "Failed to ping server")
return false
}
val time = System.currentTimeMillis()
if (androidacyBlockade > time) return true // fake it till you make it. Basically,
// don't fail just because we're rate limited. API and web rate limits are different.
androidacyBlockade = time + 30000L
try {
if (token == null) {
token = getSharedPreferences("androidacy")?.getString("pref_androidacy_api_token", null)
if (token != null && !isValidToken(token)) {
Timber.i("Token expired or invalid, requesting new one...")
token = null
} else {
Timber.i("Using cached token")
}
} else if (!isValidToken(token)) {
Timber.i("Token expired, requesting new one...")
token = null
} else {
Timber.i("Using validated cached token")
}
} catch (e: IOException) {
if (shouldTimeout(e)) {
Timber.e(e, "We are being rate limited!")
androidacyBlockade = time + 3600000L
}
return false
}
if (token == null) {
Timber.i("Token is null, requesting new one...")
try {
Timber.i("Requesting new token...")
// POST json request to https://production-api.androidacy.com/auth/register
token = requestNewToken()
// Parse token
try {
val jsonObject = JSONObject(token!!)
// log last four of token, replacing the rest with asterisks
token = jsonObject.getString("token")
val tempToken = token!!
Timber.d(
"Token: %s",
tempToken.substring(0, tempToken.length - 4)
.replace(".".toRegex(), "*") + tempToken.substring(
tempToken.length - 4
)
)
memberLevel = jsonObject.getString("role")
Timber.d("Member level: %s", memberLevel)
} catch (e: JSONException) {
Timber.e(e, "Failed to parse token: %s", token)
// Show a toast
val mainLooper = Looper.getMainLooper()
val handler = Handler(mainLooper)
handler.post {
Toast.makeText(
INSTANCE,
R.string.androidacy_failed_to_parse_token,
Toast.LENGTH_LONG
).show()
}
return false
}
// Ensure token is valid
if (!isValidToken(token)) {
Timber.e("Failed to validate token")
// Show a toast
val mainLooper = Looper.getMainLooper()
val handler = Handler(mainLooper)
handler.post {
Toast.makeText(
INSTANCE,
R.string.androidacy_failed_to_validate_token,
Toast.LENGTH_LONG
).show()
}
return false
} else {
// Save token to shared preference
val editor = getSharedPreferences("androidacy")!!.edit()
editor.putString("pref_androidacy_api_token", token)
editor.apply()
Timber.i("Token saved to shared preference")
}
} catch (e: Exception) {
if (shouldTimeout(e)) {
Timber.e(e, "We are being rate limited!")
androidacyBlockade = time + 3600000L
}
Timber.e(e, "Failed to get a new token")
return false
}
}
return true
}
@Suppress("NAME_SHADOWING")
@Throws(JSONException::class)
override fun populate(jsonObject: JSONObject): List<RepoModule>? {
var jsonObject = jsonObject
Timber.d("AndroidacyRepoData populate start")
val name = jsonObject.optString("name", "Androidacy Modules Repo")
val nameForModules =
if (name.endsWith(" (Official)")) name.substring(0, name.length - 11) else name
val jsonArray: JSONArray = try {
jsonObject.getJSONArray("data")
} catch (e: JSONException) {
// probably using modules key since it's cached
try {
jsonObject.getJSONArray("modules")
} catch (e2: JSONException) {
// we should never get here, bail out
Timber.e(e2, "Failed to parse modules")
return null
}
}
for (repoModule in moduleHashMap.values) {
repoModule.processed = false
}
val newModules = ArrayList<RepoModule>()
val len = jsonArray.length()
var lastLastUpdate: Long = 0
for (i in 0 until len) {
jsonObject = jsonArray.getJSONObject(i)
val moduleId: String = try {
jsonObject.getString("codename")
} catch (e: JSONException) {
Timber.e(
"Module %s has no codename or json %s is invalid",
jsonObject.optString("codename", "Unknown"),
jsonObject.toString()
)
continue
}
// Normally, we'd validate the module id here, but we don't need to because the server does it for us
val lastUpdate: Long = try {
jsonObject.getLong("updated_at") * 1000
} catch (e: JSONException) {
jsonObject.getLong("lastUpdate") * 1000
}
lastLastUpdate = max(lastLastUpdate, lastUpdate)
var repoModule = moduleHashMap[moduleId]
if (repoModule == null) {
repoModule = RepoModule(this, moduleId)
repoModule.moduleInfo.flags = 0
moduleHashMap[moduleId] = repoModule
newModules.add(repoModule)
} else {
if (repoModule.lastUpdated < lastUpdate) {
newModules.add(repoModule)
}
}
repoModule.processed = true
repoModule.lastUpdated = lastUpdate
repoModule.repoName = nameForModules
repoModule.zipUrl = filterURL(jsonObject.optString("zipUrl", ""))
repoModule.notesUrl = filterURL(jsonObject.optString("notesUrl", ""))
if (repoModule.zipUrl == null) {
repoModule.zipUrl = // Fallback url in case the API doesn't have zipUrl
"https://$host/magisk/info/$moduleId"
}
if (repoModule.notesUrl == null) {
repoModule.notesUrl = // Fallback url in case the API doesn't have notesUrl
"https://$host/magisk/readme/$moduleId"
}
repoModule.zipUrl = injectToken(repoModule.zipUrl)
repoModule.notesUrl = injectToken(repoModule.notesUrl)
repoModule.qualityText = R.string.module_downloads
repoModule.qualityValue = jsonObject.optInt("downloads", 0)
if (repoModule.qualityValue == 0) {
repoModule.qualityValue = jsonObject.optInt("stats", 0)
}
val checksum = jsonObject.optString("checksum", "")
repoModule.checksum = checksum.ifEmpty { null }
val moduleInfo = repoModule.moduleInfo
moduleInfo.name = jsonObject.getString("name")
moduleInfo.versionCode = jsonObject.getLong("versionCode")
moduleInfo.version = jsonObject.optString("version", "v" + moduleInfo.versionCode)
moduleInfo.author = jsonObject.optString("author", "Unknown")
moduleInfo.description = jsonObject.optString("description", "")
moduleInfo.minApi = jsonObject.getInt("minApi")
moduleInfo.maxApi = jsonObject.getInt("maxApi")
val minMagisk = jsonObject.getString("minMagisk")
try {
val c = minMagisk.indexOf('.')
if (c == -1) {
moduleInfo.minMagisk = minMagisk.toInt()
} else {
moduleInfo.minMagisk = // Allow 24.1 to mean 24100
minMagisk.substring(0, c).toInt() * 1000 + minMagisk.substring(c + 1)
.toInt() * 100
}
} catch (e: Exception) {
moduleInfo.minMagisk = 0
}
moduleInfo.needRamdisk = jsonObject.optBoolean("needRamdisk", false)
moduleInfo.changeBoot = jsonObject.optBoolean("changeBoot", false)
moduleInfo.mmtReborn = jsonObject.optBoolean("mmtReborn", false)
moduleInfo.support = filterURL(jsonObject.optString("support"))
moduleInfo.donate = filterURL(jsonObject.optString("donate"))
moduleInfo.safe = jsonObject.has("vt_status") && jsonObject.getString("vt_status")
.equals("clean", ignoreCase = true) || jsonObject.optBoolean("safe", false)
val config = jsonObject.optString("config", "")
moduleInfo.config = config.ifEmpty { null }
applyFallbacks(moduleInfo) // Apply fallbacks
}
val moduleInfoIterator = moduleHashMap.values.iterator()
while (moduleInfoIterator.hasNext()) {
val repoModule = moduleInfoIterator.next()
if (!repoModule.processed) {
moduleInfoIterator.remove()
} else {
repoModule.moduleInfo.verify()
}
}
lastUpdate = lastLastUpdate
this.name = name
website = jsonObject.optString("website")
support = jsonObject.optString("support")
donate = jsonObject.optString("donate")
submitModule = jsonObject.optString("submitModule")
return newModules
}
override fun storeMetadata(repoModule: RepoModule, data: ByteArray?) {}
override fun tryLoadMetadata(repoModule: RepoModule): Boolean {
if (moduleHashMap.containsKey(repoModule.id)) {
repoModule.moduleInfo.flags =
repoModule.moduleInfo.flags and ModuleInfo.FLAG_METADATA_INVALID.inv()
return true
}
repoModule.moduleInfo.flags =
repoModule.moduleInfo.flags or ModuleInfo.FLAG_METADATA_INVALID
return false
}
override fun getUrl(): String {
return if (token == null) url else url + "?token=" + token + "&v=" + BuildConfig.VERSION_CODE + "&c=" + BuildConfig.VERSION_NAME + "&device_id=" + generateDeviceId() + "&client_id=" + BuildConfig.ANDROIDACY_CLIENT_ID
}
@Suppress("NAME_SHADOWING")
private fun injectToken(url: String?): String? {
// Do not inject token for non Androidacy urls
var url = url
if (!isAndroidacyLink(url)) return url
if (testMode) {
if (url!!.startsWith("https://production-api.androidacy.com/")) {
Timber.e("Got non test mode url: %s", hideToken(url))
url = "https://staging-api.androidacy.com/" + url.substring(38)
}
} else {
if (url!!.startsWith("https://staging-api.androidacy.com/")) {
Timber.e("Got test mode url: %s", hideToken(url))
url = "https://production-api.androidacy.com/" + url.substring(35)
}
}
val token = "token=$token"
val deviceId = "device_id=" + generateDeviceId()
if (!url.contains(token)) {
return if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
"$url&$token"
} else {
"$url?$token"
}
}
return if (!url.contains(deviceId)) {
if (url.lastIndexOf('/') < url.lastIndexOf('?')) {
"$url&$deviceId"
} else {
"$url?$deviceId"
}
} else url
}
override var name: String?
get() = if (testMode) super.name + " (Test Mode)" else super.name!!
set(name) {
super.name = name
}
fun setToken(token: String?) {
if (hasWebView()) {
Companion.token = token
}
}
companion object {
private var ANDROIDACY_DEVICE_ID: String? = null
var token = getSharedPreferences("androidacy")!!.getString("pref_androidacy_api_token", null)
init {
@Suppress("LocalVariableName") val OK_HTTP_URL_BUILDER: Builder = Builder().scheme("https")
// Using HttpUrl.Builder.host(String) crash the app
OK_HTTP_URL_BUILDER.host("production-api.androidacy.com")
OK_HTTP_URL_BUILDER.build()
}
@JvmStatic
val instance: AndroidacyRepoData
get() = RepoManager.getINSTANCE()!!.androidacyRepoData!!
private fun filterURL(url: String?): String? {
return if (url.isNullOrEmpty() || isInvalidURL(url)) {
null
} else url
}
// Generates a unique device ID. This is used to identify the device in the API for rate
// limiting and fraud detection.
fun generateDeviceId(): String? {
// first, check if ANDROIDACY_DEVICE_ID is already set
if (ANDROIDACY_DEVICE_ID != null) {
return ANDROIDACY_DEVICE_ID
}
// Try to get the device ID from the shared preferences
val sharedPreferences = getSharedPreferences("androidacy")
val deviceIdPref =
sharedPreferences!!.getString("device_id_v2", null)
return if (deviceIdPref != null) {
ANDROIDACY_DEVICE_ID = deviceIdPref
deviceIdPref
} else {
val fp = create(INSTANCE!!.applicationContext)
fp.getFingerprint(Fingerprinter.Version.V_5) { fingerprint: String? ->
ANDROIDACY_DEVICE_ID = fingerprint
// use fingerprint
// Save the device ID to the shared preferences
val editor = sharedPreferences.edit()
editor.putString("device_id_v2", ANDROIDACY_DEVICE_ID)
editor.apply()
}
// wait for up to 5 seconds for the fingerprint to be generated (ANDROIDACY_DEVICE_ID to be set)
val startTime = System.currentTimeMillis()
while (ANDROIDACY_DEVICE_ID == null && System.currentTimeMillis() - startTime < 5000) {
try {
Thread.sleep(100)
} catch (ignored: InterruptedException) {
Thread.currentThread().interrupt()
}
}
if (ANDROIDACY_DEVICE_ID == null) {
// fingerprint generation failed, use a random UUID
ANDROIDACY_DEVICE_ID = UUID.randomUUID().toString()
}
ANDROIDACY_DEVICE_ID
}
}
}
}

@ -86,7 +86,7 @@ class AndroidacyWebAPI(
return
}
downloadMode = false
val repoModule = AndroidacyRepoData.getInstance().moduleHashMap[installTitle]
val repoModule = AndroidacyRepoData.instance.moduleHashMap[installTitle]
val title: String?
var description: String?
var mmtReborn = false
@ -293,7 +293,7 @@ class AndroidacyWebAPI(
openNativeModuleDialogRaw(moduleUrl, moduleId, installTitle, checksum, true)
}
} else {
val repoModule = AndroidacyRepoData.getInstance().moduleHashMap[installTitle]
val repoModule = AndroidacyRepoData.instance.moduleHashMap[installTitle]
var config: String? = null
var mmtReborn = false
if (repoModule != null && Objects.requireNonNull<String?>(repoModule.moduleInfo.name).length >= 3) {
@ -545,7 +545,7 @@ class AndroidacyWebAPI(
@JavascriptInterface
fun setAndroidacyToken(token: String?) {
AndroidacyRepoData.getInstance().setToken(token)
AndroidacyRepoData.instance.setToken(token)
}
// Androidacy feature level declaration method

@ -180,10 +180,10 @@ class BackgroundUpdateChecker(context: Context, workerParams: WorkerParameters)
Timber.d("Not posting notification because of missing permission")
}
ModuleManager.instance!!.scanAsync()
RepoManager.getINSTANCE().update(null)
RepoManager.getINSTANCE()!!.update(null)
ModuleManager.instance!!.runAfterScan {
var moduleUpdateCount = 0
val repoModules = RepoManager.getINSTANCE().modules
val repoModules = RepoManager.getINSTANCE()!!.modules
// hashmap of updateable modules names
val updateableModules = HashMap<String, String>()
for (localModuleInfo in ModuleManager.instance!!.modules.values) {

@ -86,7 +86,7 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
}
public String getUpdateZipRepo() {
return this.moduleInfo == null || (this.repoModule != null && this.moduleInfo.updateVersionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.repoData.id : "update_json";
return this.moduleInfo == null || (this.repoModule != null && this.moduleInfo.updateVersionCode < this.repoModule.moduleInfo.versionCode) ? this.repoModule.repoData.preferenceId : "update_json";
}
public String getUpdateZipChecksum() {
@ -143,17 +143,17 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
} else if (this.moduleInfo.versionCode < this.moduleInfo.updateVersionCode || (this.repoModule != null && this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode)) {
boolean ignoreUpdate = false;
try {
if (Objects.requireNonNull(MainApplication.getSharedPreferences("mmm").getStringSet("pref_background_update_check_excludes", new HashSet<>())).contains(moduleInfo.id))
if (Objects.requireNonNull(Objects.requireNonNull(MainApplication.getSharedPreferences("mmm")).getStringSet("pref_background_update_check_excludes", new HashSet<>())).contains(moduleInfo.id))
ignoreUpdate = true;
} catch (Exception ignored) {
}
// now, we just had to make it more fucking complicated, didn't we?
// we now have pref_background_update_check_excludes_version, which is a id:version stringset of versions the user may want to "skip"
// oh, and because i hate myself, i made ^ at the beginning match that version and newer, and $ at the end match that version and older
Set<String> stringSetT = MainApplication.getSharedPreferences("mmm").getStringSet("pref_background_update_check_excludes_version", new HashSet<>());
Set<String> stringSetT = Objects.requireNonNull(MainApplication.getSharedPreferences("mmm")).getStringSet("pref_background_update_check_excludes_version", new HashSet<>());
String version = "";
Timber.d(stringSetT.toString());
// unfortunately, stringsett.contains() doesn't work for partial matches
// unfortunately, stringset.contains() doesn't work for partial matches
// so we have to iterate through the set
for (String s : stringSetT) {
if (s.startsWith(this.moduleInfo.id)) {
@ -201,7 +201,7 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
Timber.d("Module %s has update, but is ignored", this.moduleId);
return Type.INSTALLABLE;
} else {
MainApplication.getINSTANCE().modulesHaveUpdates = true;
Objects.requireNonNull(MainApplication.getINSTANCE()).modulesHaveUpdates = true;
if (!MainApplication.getINSTANCE().updateModules.contains(this.moduleId)) {
MainApplication.getINSTANCE().updateModules.add(this.moduleId);
MainApplication.getINSTANCE().updateModuleCount++;

@ -77,7 +77,7 @@ class ModuleViewListBuilder(private val activity: Activity) {
moduleHolder.repoModule = null
}
val repoManager = RepoManager.getINSTANCE()
repoManager.runAfterUpdate {
repoManager?.runAfterUpdate {
Timber.i("A2: %s", repoManager.modules.size)
val no32bitSupport = Build.SUPPORTED_32_BIT_ABIS.isEmpty()
for (repoModule in repoManager.modules.values) {
@ -92,7 +92,7 @@ class ModuleViewListBuilder(private val activity: Activity) {
if (!repoModule.repoData.isEnabled) {
Timber.i(
"Repo %s is disabled, skipping module %s",
repoModule.repoData.id,
repoModule.repoData.preferenceId,
repoModule.id
)
continue
@ -162,7 +162,7 @@ class ModuleViewListBuilder(private val activity: Activity) {
if (updating) return
updating = true
instance!!.afterScan()
RepoManager.getINSTANCE().afterUpdate()
RepoManager.getINSTANCE()!!.afterUpdate()
val moduleHolders: ArrayList<ModuleHolder>
val newNotificationsLen: Int
val first: Boolean

@ -1,63 +0,0 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.repo;
import com.fox2code.mmm.utils.io.net.Http;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public final class CustomRepoData extends RepoData {
boolean loadedExternal;
String override;
CustomRepoData(String url, File cacheRoot) {
super(url, cacheRoot);
}
@Override
public boolean isEnabledByDefault() {
return this.override != null || this.loadedExternal;
}
@Override
public String getPreferenceId() {
return this.override == null ?
this.id : this.override;
}
public void quickPrePopulate() throws IOException, JSONException {
JSONObject jsonObject = new JSONObject(
new String(Http.doHttpGet(this.getUrl(),
false), StandardCharsets.UTF_8));
// make sure there's at least a name and a modules or data object
if (!jsonObject.has("name") || (!jsonObject.has("modules") && !jsonObject.has("data"))) {
throw new IllegalArgumentException("Invalid repo: " + this.getUrl());
}
this.name = jsonObject.getString("name").trim();
this.website = jsonObject.optString("website");
this.support = jsonObject.optString("support");
this.donate = jsonObject.optString("donate");
this.submitModule = jsonObject.optString("submitModule");
}
public Object toJSON() {
try {
return new JSONObject()
.put("id", this.id)
.put("name", this.name)
.put("website", this.website)
.put("support", this.support)
.put("donate", this.donate)
.put("submitModule", this.submitModule);
} catch (JSONException ignored) {
return null;
}
}
}

@ -0,0 +1,55 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.repo
import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet
import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.io.IOException
import java.nio.charset.StandardCharsets
class CustomRepoData internal constructor(url: String?, cacheRoot: File?) : RepoData(
url!!, cacheRoot!!
) {
@JvmField
var loadedExternal = false
@JvmField
var override: String? = null
override val isEnabledByDefault: Boolean
get() = override != null || loadedExternal
@Throws(IOException::class, JSONException::class)
fun quickPrePopulate() {
val jsonObject = JSONObject(
String(
doHttpGet(
getUrl()!!,
false
), StandardCharsets.UTF_8
)
)
// make sure there's at least a name and a modules or data object
require(!(!jsonObject.has("name") || !jsonObject.has("modules") && !jsonObject.has("data"))) { "Invalid repo: " + getUrl() }
name = jsonObject.getString("name").trim { it <= ' ' }
website = jsonObject.optString("website")
support = jsonObject.optString("support")
donate = jsonObject.optString("donate")
submitModule = jsonObject.optString("submitModule")
}
fun toJSON(): Any? {
return try {
JSONObject()
.put("id", preferenceId)
.put("name", name)
.put("website", website)
.put("support", support)
.put("donate", donate)
.put("submitModule", submitModule)
} catch (ignored: JSONException) {
null
}
}
}

@ -1,207 +0,0 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.repo;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.utils.io.Hashes;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.realm.ReposList;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import timber.log.Timber;
public class CustomRepoManager {
public static final int MAX_CUSTOM_REPOS = 5;
private static final boolean AUTO_RECOMPILE = true;
private final RepoManager repoManager;
private final String[] customRepos;
boolean dirty;
private int customReposCount;
@SuppressWarnings("unused")
CustomRepoManager(MainApplication mainApplication, RepoManager repoManager) {
this.repoManager = repoManager;
this.customRepos = new String[MAX_CUSTOM_REPOS];
this.customReposCount = 0;
// refuse to load if setup is not complete
if (MainApplication.getSharedPreferences("mmm").getString("last_shown_setup", "").equals("")) {
return;
}
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
if (realm.isInTransaction()) {
realm.commitTransaction();
}
int i = 0;
@SuppressWarnings("MismatchedReadAndWriteOfArray") final int[] lastFilled = {0};
realm.executeTransaction(realm1 -> {
// find all repos that are not built-in
for (ReposList reposList : realm1.where(ReposList.class).notEqualTo("id", "androidacy_repo").and().notEqualTo("id", "magisk_alt_repo").and().notEqualTo("id", "magisk_official_repo").findAll()) {
String repo = reposList.getUrl();
if (!PropUtils.isNullString(repo) && !RepoManager.isBuiltInRepo(repo)) {
lastFilled[0] = i;
int index = AUTO_RECOMPILE ? this.customReposCount : i;
this.customRepos[index] = repo;
this.customReposCount++;
((CustomRepoData) this.repoManager.addOrGet(repo)).override = "custom_repo_" + index;
}
}
});
}
public CustomRepoData addRepo(String repo) {
if (RepoManager.isBuiltInRepo(repo))
throw new IllegalArgumentException("Can't add built-in repo to custom repos");
for (String repoEntry : this.customRepos) {
if (repo.equals(repoEntry)) return (CustomRepoData) this.repoManager.get(repoEntry);
}
int i = 0;
while (customRepos[i] != null) i++;
customRepos[i] = repo;
// fetch that sweet sweet json
byte[] json;
try {
json = Http.doHttpGet(repo, false);
} catch (Exception e) {
Timber.e(e, "Failed to fetch json from repo");
return null;
}
// get website, support, donate, submitModule. all optional. name is required.
// parse json
JSONObject jsonObject;
try {
jsonObject = new JSONObject(new String(json));
} catch (Exception e) {
Timber.e(e, "Failed to parse json from repo");
return null;
}
// get name
String name;
try {
name = jsonObject.getString("name");
} catch (Exception e) {
Timber.e(e, "Failed to get name from json");
return null;
}
// get website
String website;
try {
website = jsonObject.getString("website");
} catch (Exception e) {
website = null;
}
// get support
String support;
try {
support = jsonObject.getString("support");
} catch (Exception e) {
support = null;
}
// get donate
String donate;
try {
donate = jsonObject.getString("donate");
} catch (Exception e) {
donate = null;
}
// get submitModule
String submitModule;
try {
submitModule = jsonObject.getString("submitModule");
} catch (Exception e) {
submitModule = null;
}
String id = "repo_" + Hashes.hashSha256(repo.getBytes(StandardCharsets.UTF_8));
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
String finalWebsite = website;
String finalSupport = support;
String finalDonate = donate;
String finalSubmitModule = submitModule;
realm.executeTransaction(realm1 -> {
// find the matching entry for repo_0, repo_1, etc.
ReposList reposList = realm1.where(ReposList.class).equalTo("id", id).findFirst();
if (reposList == null) {
reposList = realm1.createObject(ReposList.class, id);
}
reposList.setUrl(repo);
reposList.setName(name);
reposList.setWebsite(finalWebsite);
reposList.setSupport(finalSupport);
reposList.setDonate(finalDonate);
reposList.setSubmitModule(finalSubmitModule);
reposList.setEnabled(true);
// save the object
realm1.copyToRealmOrUpdate(reposList);
});
customReposCount++;
this.dirty = true;
CustomRepoData customRepoData = (CustomRepoData) this.repoManager.addOrGet(repo);
customRepoData.override = "repo_" + id;
customRepoData.id = id;
customRepoData.website = website;
customRepoData.support = support;
customRepoData.donate = donate;
customRepoData.submitModule = submitModule;
customRepoData.name = name;
// Set the enabled state to true
customRepoData.setEnabled(true);
customRepoData.updateEnabledState();
realm.close();
return customRepoData;
}
public CustomRepoData getRepo(String id) {
return (CustomRepoData) this.repoManager.get(id);
}
public void removeRepo(int index) {
String oldRepo = customRepos[index];
if (oldRepo != null) {
customRepos[index] = null;
customReposCount--;
CustomRepoData customRepoData = (CustomRepoData) this.repoManager.get(oldRepo);
if (customRepoData != null) {
customRepoData.setEnabled(false);
customRepoData.override = null;
}
this.dirty = true;
}
}
public boolean hasRepo(String repo) {
for (String repoEntry : this.customRepos) {
if (repo.equals(repoEntry)) return true;
}
return false;
}
public boolean canAddRepo() {
return this.customReposCount < MAX_CUSTOM_REPOS;
}
public boolean canAddRepo(String repo) {
if (RepoManager.isBuiltInRepo(repo) || this.hasRepo(repo) || !this.canAddRepo())
return false;
return repo.startsWith("https://") && repo.indexOf('/', 9) != -1;
}
public int getRepoCount() {
return this.customReposCount;
}
public boolean needUpdate() {
boolean needUpdate = this.dirty;
if (needUpdate) this.dirty = false;
return needUpdate;
}
}

@ -0,0 +1,208 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.repo
import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.MainApplication.Companion.INSTANCE
import com.fox2code.mmm.MainApplication.Companion.getSharedPreferences
import com.fox2code.mmm.utils.io.Hashes.Companion.hashSha256
import com.fox2code.mmm.utils.io.PropUtils.Companion.isNullString
import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet
import com.fox2code.mmm.utils.realm.ReposList
import io.realm.Realm
import io.realm.RealmConfiguration
import org.json.JSONObject
import timber.log.Timber
import java.nio.charset.StandardCharsets
@Suppress("UNUSED_PARAMETER", "MemberVisibilityCanBePrivate")
class CustomRepoManager internal constructor(
mainApplication: MainApplication?,
private val repoManager: RepoManager
) {
private val customRepos: Array<String?> = arrayOfNulls(MAX_CUSTOM_REPOS)
@JvmField
var dirty = false
var repoCount: Int
private set
init {
repoCount = 0
// refuse to load if setup is not complete
if (getSharedPreferences("mmm")!!.getString("last_shown_setup", "") != "") {
val realmConfiguration =
RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(
INSTANCE!!.key
).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(
INSTANCE!!.getDataDirWithPath("realms")
).schemaVersion(1).build()
val realm = Realm.getInstance(realmConfiguration)
if (realm.isInTransaction) {
realm.commitTransaction()
}
val i = 0
val lastFilled = intArrayOf(0)
realm.executeTransaction { realm1: Realm ->
// find all repos that are not built-in
for (reposList in realm1.where(ReposList::class.java)
.notEqualTo("id", "androidacy_repo").and().notEqualTo("id", "magisk_alt_repo")
.and()
.notEqualTo("id", "magisk_official_repo").findAll()) {
val repo = reposList.url
if (!isNullString(repo) && !RepoManager.isBuiltInRepo(repo)) {
lastFilled[0] = i
val index = if (AUTO_RECOMPILE) repoCount else i
customRepos[index] = repo
repoCount++
(repoManager.addOrGet(repo) as CustomRepoData).override =
"custom_repo_$index"
}
}
}
}
}
fun addRepo(repo: String): CustomRepoData? {
require(!RepoManager.isBuiltInRepo(repo)) { "Can't add built-in repo to custom repos" }
for (repoEntry in customRepos) {
if (repo == repoEntry) return repoManager[repoEntry] as CustomRepoData
}
var i = 0
while (customRepos[i] != null) i++
customRepos[i] = repo
// fetch that sweet sweet json
val json: ByteArray = try {
doHttpGet(repo, false)
} catch (e: Exception) {
Timber.e(e, "Failed to fetch json from repo")
return null
}
// get website, support, donate, submitModule. all optional. name is required.
// parse json
val jsonObject: JSONObject = try {
JSONObject(String(json))
} catch (e: Exception) {
Timber.e(e, "Failed to parse json from repo")
return null
}
// get name
val name: String = try {
jsonObject.getString("name")
} catch (e: Exception) {
Timber.e(e, "Failed to get name from json")
return null
}
// get website
val website: String? = try {
jsonObject.getString("website")
} catch (e: Exception) {
null
}
// get support
val support: String? = try {
jsonObject.getString("support")
} catch (e: Exception) {
null
}
// get donate
val donate: String? = try {
jsonObject.getString("donate")
} catch (e: Exception) {
null
}
// get submitModule
val submitModule: String? = try {
jsonObject.getString("submitModule")
} catch (e: Exception) {
null
}
val id = "repo_" + hashSha256(repo.toByteArray(StandardCharsets.UTF_8))
val realmConfiguration = RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(
INSTANCE!!.key
).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(
INSTANCE!!.getDataDirWithPath("realms")
).schemaVersion(1).build()
val realm = Realm.getInstance(realmConfiguration)
realm.executeTransaction { realm1: Realm ->
// find the matching entry for repo_0, repo_1, etc.
var reposList =
realm1.where(ReposList::class.java).equalTo("id", id).findFirst()
if (reposList == null) {
reposList = realm1.createObject(ReposList::class.java, id)
}
reposList!!.url = repo
reposList.name = name
reposList.website = website
reposList.support = support
reposList.donate = donate
reposList.submitModule = submitModule
reposList.isEnabled = true
// save the object
realm1.copyToRealmOrUpdate(reposList)
}
repoCount++
dirty = true
val customRepoData = repoManager.addOrGet(repo) as CustomRepoData
customRepoData.override = "repo_$id"
customRepoData.preferenceId = id
customRepoData.website = website
customRepoData.support = support
customRepoData.donate = donate
customRepoData.submitModule = submitModule
customRepoData.name = name
// Set the enabled state to true
customRepoData.isEnabled = true
customRepoData.updateEnabledState()
realm.close()
return customRepoData
}
fun getRepo(id: String?): CustomRepoData {
return repoManager[id] as CustomRepoData
}
@Suppress("SENSELESS_COMPARISON")
fun removeRepo(index: Int) {
val oldRepo = customRepos[index]
if (oldRepo != null) {
customRepos[index] = null
repoCount--
val customRepoData = repoManager[oldRepo] as CustomRepoData
if (customRepoData != null) {
customRepoData.isEnabled = false
customRepoData.override = null
}
dirty = true
}
}
fun hasRepo(repo: String): Boolean {
for (repoEntry in customRepos) {
if (repo == repoEntry) return true
}
return false
}
fun canAddRepo(): Boolean {
return repoCount < MAX_CUSTOM_REPOS
}
fun canAddRepo(repo: String): Boolean {
return if (RepoManager.isBuiltInRepo(repo) || hasRepo(repo) || !this.canAddRepo()) false else repo.startsWith(
"https://"
) && repo.indexOf('/', 9) != -1
}
fun needUpdate(): Boolean {
val needUpdate = dirty
if (needUpdate) dirty = false
return needUpdate
}
companion object {
const val MAX_CUSTOM_REPOS = 5
private const val AUTO_RECOMPILE = true
}
}

@ -1,429 +0,0 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.repo;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.fox2code.mmm.AppUpdateManager;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainActivity;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.XRepo;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.realm.ModuleListCache;
import com.fox2code.mmm.utils.realm.ReposList;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import io.realm.Realm;
import io.realm.RealmConfiguration;
import io.realm.RealmResults;
import timber.log.Timber;
public class RepoData extends XRepo {
public final JSONObject supportedProperties = new JSONObject();
private final Object populateLock = new Object();
public String url;
public String id;
public File cacheRoot;
public HashMap<String, RepoModule> moduleHashMap;
public JSONObject metaDataCache;
public long lastUpdate;
public String name, website, support, donate, submitModule;
protected String defaultName, defaultWebsite, defaultSupport, defaultDonate, defaultSubmitModule;
// array with module info default values
// supported properties for a module
//id=<string>
//name=<string>
//version=<string>
//versionCode=<int>
//author=<string>
//description=<string>
//minApi=<int>
//maxApi=<int>
//minMagisk=<int>
//needRamdisk=<boolean>
//support=<url>
//donate=<url>
//config=<package>
//changeBoot=<boolean>
//mmtReborn=<boolean>
// extra properties only useful for the database
//repoId=<string>
//installed=<boolean>
//installedVersionCode=<int> (only if installed)
private boolean forceHide, enabled; // Cache for speed
public RepoData(String url, File cacheRoot) {
// setup supportedProperties
try {
supportedProperties.put("id", "");
supportedProperties.put("name", "");
supportedProperties.put("version", "");
supportedProperties.put("versionCode", "");
supportedProperties.put("author", "");
supportedProperties.put("description", "");
supportedProperties.put("minApi", "");
supportedProperties.put("maxApi", "");
supportedProperties.put("minMagisk", "");
supportedProperties.put("needRamdisk", "");
supportedProperties.put("support", "");
supportedProperties.put("donate", "");
supportedProperties.put("config", "");
supportedProperties.put("changeBoot", "");
supportedProperties.put("mmtReborn", "");
supportedProperties.put("repoId", "");
supportedProperties.put("installed", "");
supportedProperties.put("installedVersionCode", "");
supportedProperties.put("safe", "");
} catch (JSONException e) {
Timber.e(e, "Error while setting up supportedProperties");
}
this.url = url;
this.id = RepoManager.internalIdOfUrl(url);
this.cacheRoot = cacheRoot;
// metadata cache is a realm database from ModuleListCache
this.metaDataCache = null;
this.moduleHashMap = new HashMap<>();
this.defaultName = url; // Set url as default name
this.forceHide = AppUpdateManager.Companion.shouldForceHide(getPreferenceId());
// this.enable is set from the database
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
ReposList reposList = realm.where(ReposList.class).equalTo("id", this.id).findFirst();
if (reposList == null) {
Timber.d("RepoData for %s not found in database", this.id);
// log every repo in db
Object[] fullList = realm.where(ReposList.class).findAll().toArray();
Timber.d("RepoData: " + this.id + ". repos in database: " + fullList.length);
for (Object repo : fullList) {
ReposList r = (ReposList) repo;
Timber.d("RepoData: " + this.id + ". repo: " + r.getId() + " " + r.getName() + " " + r.getWebsite() + " " + r.getSupport() + " " + r.getDonate() + " " + r.getSubmitModule() + " " + r.isEnabled());
}
} else {
Timber.d("RepoData for %s found in database", this.id);
}
Timber.d("RepoData: " + this.id + ". record in database: " + (reposList != null ? reposList.toString() : "none"));
this.enabled = (!this.forceHide && reposList != null && reposList.isEnabled());
this.defaultWebsite = "https://" + Uri.parse(url).getHost() + "/";
// open realm database
// load metadata from realm database
if (this.enabled) {
try {
this.metaDataCache = ModuleListCache.getRepoModulesAsJson(this.id);
// log count of modules in the database
Timber.d("RepoData: " + this.id + ". modules in database: " + this.metaDataCache.length());
// load repo metadata from ReposList unless it's a built-in repo
if (RepoManager.isBuiltInRepo(this.id)) {
this.name = this.defaultName;
this.website = this.defaultWebsite;
this.support = this.defaultSupport;
this.donate = this.defaultDonate;
this.submitModule = this.defaultSubmitModule;
} else {
// get everything from ReposList realm database
this.name = Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", this.id).findFirst()).getName();
this.website = Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", this.id).findFirst()).getWebsite();
this.support = Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", this.id).findFirst()).getSupport();
this.donate = Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", this.id).findFirst()).getDonate();
this.submitModule = Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", this.id).findFirst()).getSubmitModule();
}
} catch (Exception e) {
Timber.w("Failed to load repo metadata from database: " + e.getMessage() + ". If this is a first time run, this is normal.");
}
}
realm.close();
}
private static boolean isNonNull(String str) {
return str != null && !str.isEmpty() && !"null".equals(str);
}
protected boolean prepare() {
return true;
}
protected List<RepoModule> populate(JSONObject jsonObject) throws JSONException {
List<RepoModule> newModules = new ArrayList<>();
synchronized (this.populateLock) {
String name = jsonObject.getString("name").trim();
// if Official is present, remove it, or (Official), or [Official]. We don't want to show it in the UI
String nameForModules = name.endsWith(" (Official)") ? name.substring(0, name.length() - 11) : name;
nameForModules = nameForModules.endsWith(" [Official]") ? nameForModules.substring(0, nameForModules.length() - 11) : nameForModules;
nameForModules = nameForModules.contains("Official") ? nameForModules.replace("Official", "").trim() : nameForModules;
long lastUpdate = jsonObject.getLong("last_update");
for (RepoModule repoModule : this.moduleHashMap.values()) {
repoModule.processed = false;
}
JSONArray array = jsonObject.getJSONArray("modules");
int len = array.length();
for (int i = 0; i < len; i++) {
JSONObject module = array.getJSONObject(i);
String moduleId = module.getString("id");
// module IDs must match the regex ^[a-zA-Z][a-zA-Z0-9._-]+$ and cannot be empty or null or equal ak3-helper
if (moduleId.isEmpty() || moduleId.equals("ak3-helper") || !moduleId.matches("^[a-zA-Z][a-zA-Z0-9._-]+$")) {
continue;
}
// If module id start with a dot, warn user
if (moduleId.charAt(0) == '.') {
Timber.w("This is not recommended and may indicate an attempt to hide the module");
}
long moduleLastUpdate = module.getLong("last_update");
String moduleNotesUrl = module.getString("notes_url");
String modulePropsUrl = module.getString("prop_url");
String moduleZipUrl = module.getString("zip_url");
String moduleChecksum = module.optString("checksum");
String moduleStars = module.optString("stars");
String moduleDownloads = module.optString("downloads");
// if downloads is mull or empty, try to get it from the stats field
if (moduleDownloads.isEmpty() && module.has("stats")) {
moduleDownloads = module.optString("stats");
}
RepoModule repoModule = this.moduleHashMap.get(moduleId);
if (repoModule == null) {
repoModule = new RepoModule(this, moduleId);
this.moduleHashMap.put(moduleId, repoModule);
newModules.add(repoModule);
} else {
if (repoModule.lastUpdated < moduleLastUpdate || repoModule.moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
newModules.add(repoModule);
}
}
repoModule.processed = true;
repoModule.repoName = nameForModules;
repoModule.lastUpdated = moduleLastUpdate;
repoModule.notesUrl = moduleNotesUrl;
repoModule.propUrl = modulePropsUrl;
repoModule.zipUrl = moduleZipUrl;
repoModule.checksum = moduleChecksum;
// safety check must be overridden per repo. only androidacy repo has this flag currently
// repoModule.safe = module.optBoolean("safe", false);
if (!moduleStars.isEmpty()) {
try {
repoModule.qualityValue = Integer.parseInt(moduleStars);
repoModule.qualityText = R.string.module_stars;
} catch (NumberFormatException ignored) {
}
} else if (!moduleDownloads.isEmpty()) {
try {
repoModule.qualityValue = Integer.parseInt(moduleDownloads);
repoModule.qualityText = R.string.module_downloads;
} catch (NumberFormatException ignored) {
}
}
}
// Remove no longer existing modules
Iterator<RepoModule> moduleInfoIterator = this.moduleHashMap.values().iterator();
while (moduleInfoIterator.hasNext()) {
RepoModule repoModule = moduleInfoIterator.next();
if (!repoModule.processed) {
boolean delete = new File(this.cacheRoot, repoModule.id + ".prop").delete();
if (!delete) {
throw new RuntimeException("Failed to delete module metadata");
}
moduleInfoIterator.remove();
} else {
repoModule.moduleInfo.verify();
}
}
// Update final metadata
this.name = name;
this.lastUpdate = lastUpdate;
this.website = jsonObject.optString("website");
this.support = jsonObject.optString("support");
this.donate = jsonObject.optString("donate");
this.submitModule = jsonObject.optString("submitModule");
}
return newModules;
}
@Override
public boolean isEnabledByDefault() {
return BuildConfig.ENABLED_REPOS.contains(this.id);
}
public void storeMetadata(RepoModule repoModule, byte[] data) throws IOException {
Files.write(new File(this.cacheRoot, repoModule.id + ".prop"), data);
}
public boolean tryLoadMetadata(RepoModule repoModule) {
File file = new File(this.cacheRoot, repoModule.id + ".prop");
if (file.exists()) {
try {
ModuleInfo moduleInfo = repoModule.moduleInfo;
PropUtils.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;
}
return true;
} catch (Exception ignored) {
boolean delete = file.delete();
if (!delete) {
throw new RuntimeException("Failed to delete invalid metadata file");
}
}
} else {
Timber.d("Metadata file not found for %s", repoModule.id);
}
repoModule.moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID;
return false;
}
@Override
public boolean isEnabled() {
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm2 = Realm.getInstance(realmConfiguration2);
AtomicBoolean dbEnabled = new AtomicBoolean(false);
realm2.executeTransaction(realm -> {
ReposList reposList = realm.where(ReposList.class).equalTo("id", this.id).findFirst();
if (reposList != null) {
dbEnabled.set(reposList.isEnabled());
} else {
// should never happen but for safety
dbEnabled.set(false);
}
});
realm2.close();
if (dbEnabled.get()) {
return !this.forceHide;
} else {
return false;
}
}
@Override
public void setEnabled(boolean enabled) {
this.enabled = enabled && !this.forceHide;
// reposlist realm
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm2 = Realm.getInstance(realmConfiguration2);
realm2.executeTransaction(realm -> {
ReposList reposList = realm.where(ReposList.class).equalTo("id", this.id).findFirst();
if (reposList != null) {
reposList.setEnabled(enabled);
}
});
realm2.close();
}
public void updateEnabledState() {
// Make sure first_launch preference is set to false
if (MainActivity.doSetupNowRunning) {
return;
}
if (this.id == null) {
Timber.e("Repo ID is null");
return;
}
// if repo starts with repo_, it's always enabled bc custom repos can't be disabled without being deleted.
this.forceHide = AppUpdateManager.Companion.shouldForceHide(getPreferenceId());
// reposlist realm
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm2 = Realm.getInstance(realmConfiguration2);
boolean dbEnabled = false;
try {
dbEnabled = Objects.requireNonNull(realm2.where(ReposList.class).equalTo("id", this.id).findFirst()).isEnabled();
} catch (Exception e) {
Timber.e(e, "Error while updating enabled state for repo %s", this.id);
}
realm2.close();
this.enabled = (!this.forceHide) && dbEnabled;
}
public String getUrl() {
return this.url;
}
public String getPreferenceId() {
return this.id;
}
// Repo data info getters
@NonNull
@Override
public String getName() {
if (isNonNull(this.name)) return this.name;
if (this.defaultName != null) return this.defaultName;
return this.url;
}
@NonNull
public String getWebsite() {
if (isNonNull(this.website)) return this.website;
if (this.defaultWebsite != null) return this.defaultWebsite;
return this.url;
}
public String getSupport() {
if (isNonNull(this.support)) return this.support;
return this.defaultSupport;
}
public String getDonate() {
if (isNonNull(this.donate)) return this.donate;
return this.defaultDonate;
}
public String getSubmitModule() {
if (isNonNull(this.submitModule)) return this.submitModule;
return this.defaultSubmitModule;
}
public final boolean isForceHide() {
return this.forceHide;
}
// should update (lastUpdate > 15 minutes)
public boolean shouldUpdate() {
Timber.d("Repo " + this.id + " should update check called");
RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm2 = Realm.getInstance(realmConfiguration2);
ReposList repo = realm2.where(ReposList.class).equalTo("id", this.id).findFirst();
// Make sure ModuleListCache for repoId is not null
File cacheRoot = MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/" + this.id);
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build();
Realm realm = Realm.getInstance(realmConfiguration);
RealmResults<ModuleListCache> moduleListCache = realm.where(ModuleListCache.class).equalTo("repoId", this.id).findAll();
if (repo != null) {
if (repo.getLastUpdate() != 0 && moduleListCache.size() != 0) {
long lastUpdate = repo.getLastUpdate();
long currentTime = System.currentTimeMillis();
long diff = currentTime - lastUpdate;
long diffMinutes = diff / (60 * 1000) % 60;
Timber.d("Repo " + this.id + " updated: " + diffMinutes + " minutes ago");
realm.close();
return diffMinutes > (BuildConfig.DEBUG ? 15 : 30);
} else {
Timber.d("Repo " + this.id + " should update could not find repo in database");
Timber.d("This is probably an error, please report this to the developer");
realm.close();
return true;
}
} else {
realm.close();
}
return true;
}
}

@ -0,0 +1,501 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.repo
import android.net.Uri
import com.fox2code.mmm.AppUpdateManager.Companion.shouldForceHide
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.MainActivity
import com.fox2code.mmm.MainApplication.Companion.INSTANCE
import com.fox2code.mmm.R
import com.fox2code.mmm.XRepo
import com.fox2code.mmm.manager.ModuleInfo
import com.fox2code.mmm.utils.io.Files.Companion.write
import com.fox2code.mmm.utils.io.PropUtils.Companion.readProperties
import com.fox2code.mmm.utils.realm.ModuleListCache
import com.fox2code.mmm.utils.realm.ReposList
import io.realm.Realm
import io.realm.RealmConfiguration
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
@Suppress("LeakingThis", "SENSELESS_COMPARISON", "RedundantSetter")
open class RepoData(url: String, cacheRoot: File) : XRepo() {
private val supportedProperties = JSONObject()
private val populateLock = Any()
@JvmField
var url: String
@JvmField
var preferenceId: String? = null
@JvmField
var cacheRoot: File
@JvmField
var moduleHashMap: HashMap<String, RepoModule>
private var metaDataCache: JSONObject?
@JvmField
var lastUpdate
: Long = 0
@JvmField
var website: String? = null
@JvmField
var support: String? = null
@JvmField
var donate: String? = null
@JvmField
var submitModule: String? = null
@JvmField
var defaultName: String
@JvmField
var defaultWebsite: String
@JvmField
protected var defaultSupport: String? = null
@JvmField
protected var defaultDonate: String? = null
@JvmField
var defaultSubmitModule: String? = null
override var name: String? = null
get() = {
// if name is null return defaultName and if defaultName is null return url
if (field == null) {
if (defaultName == null) {
url
} else {
defaultName
}
} else {
field
}
}.toString()
set(value) {
field = value
}
// array with module info default values
// supported properties for a module
//id=<string>
//name=<string>
//version=<string>
//versionCode=<int>
//author=<string>
//description=<string>
//minApi=<int>
//maxApi=<int>
//minMagisk=<int>
//needRamdisk=<boolean>
//support=<url>
//donate=<url>
//config=<package>
//changeBoot=<boolean>
//mmtReborn=<boolean>
// extra properties only useful for the database
//repoId=<string>
//installed=<boolean>
//installedVersionCode=<int> (only if installed)
var isForceHide: Boolean
private set
private var enabled // Cache for speed
: Boolean
init {
// setup supportedProperties
try {
supportedProperties.put("id", "")
supportedProperties.put("name", "")
supportedProperties.put("version", "")
supportedProperties.put("versionCode", "")
supportedProperties.put("author", "")
supportedProperties.put("description", "")
supportedProperties.put("minApi", "")
supportedProperties.put("maxApi", "")
supportedProperties.put("minMagisk", "")
supportedProperties.put("needRamdisk", "")
supportedProperties.put("support", "")
supportedProperties.put("donate", "")
supportedProperties.put("config", "")
supportedProperties.put("changeBoot", "")
supportedProperties.put("mmtReborn", "")
supportedProperties.put("repoId", "")
supportedProperties.put("installed", "")
supportedProperties.put("installedVersionCode", "")
supportedProperties.put("safe", "")
} catch (e: JSONException) {
Timber.e(e, "Error while setting up supportedProperties")
}
this.url = url
preferenceId = RepoManager.internalIdOfUrl(url)
this.cacheRoot = cacheRoot
// metadata cache is a realm database from ModuleListCache
metaDataCache = null
moduleHashMap = HashMap()
defaultName = url // Set url as default name
val tempVarForPreferenceId = preferenceId!!
isForceHide = shouldForceHide(tempVarForPreferenceId)
// this.enable is set from the database
val realmConfiguration = RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(
INSTANCE!!.key
).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(
INSTANCE!!.getDataDirWithPath("realms")
).schemaVersion(1).build()
val realm = Realm.getInstance(realmConfiguration)
val reposList = realm.where(ReposList::class.java).equalTo("id", preferenceId).findFirst()
if (reposList == null) {
Timber.d("RepoData for %s not found in database", preferenceId)
// log every repo in db
val fullList: Array<Any> = realm.where(ReposList::class.java).findAll().toTypedArray()
Timber.d("RepoData: " + preferenceId + ". repos in database: " + fullList.size)
for (repo in fullList) {
val r = repo as ReposList
Timber.d("RepoData: " + preferenceId + ". repo: " + r.id + " " + r.name + " " + r.website + " " + r.support + " " + r.donate + " " + r.submitModule + " " + r.isEnabled)
}
} else {
Timber.d("RepoData for %s found in database", preferenceId)
}
Timber.d(
"RepoData: $preferenceId. record in database: " + (reposList?.toString()
?: "none")
)
enabled = !isForceHide && reposList != null && reposList.isEnabled
defaultWebsite = "https://" + Uri.parse(url).host + "/"
// open realm database
// load metadata from realm database
if (enabled) {
try {
metaDataCache = ModuleListCache.getRepoModulesAsJson(preferenceId)
// log count of modules in the database
val tempMetaDataCacheVar = metaDataCache!!
Timber.d("RepoData: $preferenceId. modules in database: ${tempMetaDataCacheVar.length()}")
// load repo metadata from ReposList unless it's a built-in repo
if (RepoManager.isBuiltInRepo(preferenceId)) {
name = defaultName
website = defaultWebsite
support = defaultSupport
donate = defaultDonate
submitModule = defaultSubmitModule
} else {
// get everything from ReposList realm database
name = realm.where(
ReposList::class.java
).equalTo("id", preferenceId).findFirst()?.name
website = realm.where(
ReposList::class.java
).equalTo("id", preferenceId).findFirst()?.website
support = realm.where(
ReposList::class.java
).equalTo("id", preferenceId).findFirst()?.support
donate = realm.where(
ReposList::class.java
).equalTo("id", preferenceId).findFirst()?.donate
submitModule = realm.where(
ReposList::class.java
).equalTo("id", preferenceId).findFirst()?.submitModule
}
} catch (e: Exception) {
Timber.w("Failed to load repo metadata from database: " + e.message + ". If this is a first time run, this is normal.")
}
}
realm.close()
}
open fun prepare(): Boolean {
return true
}
@Throws(JSONException::class)
open fun populate(jsonObject: JSONObject): List<RepoModule>? {
val newModules: MutableList<RepoModule> = ArrayList()
synchronized(populateLock) {
val name = jsonObject.getString("name").trim { it <= ' ' }
// if Official is present, remove it, or (Official), or [Official]. We don't want to show it in the UI
var nameForModules =
if (name.endsWith(" (Official)")) name.substring(0, name.length - 11) else name
nameForModules = if (nameForModules.endsWith(" [Official]")) nameForModules.substring(
0,
nameForModules.length - 11
) else nameForModules
nameForModules =
if (nameForModules.contains("Official")) nameForModules.replace("Official", "")
.trim { it <= ' ' } else nameForModules
val lastUpdate = jsonObject.getLong("last_update")
for (repoModule in moduleHashMap.values) {
repoModule.processed = false
}
val array = jsonObject.getJSONArray("modules")
val len = array.length()
for (i in 0 until len) {
val module = array.getJSONObject(i)
val moduleId = module.getString("id")
// module IDs must match the regex ^[a-zA-Z][a-zA-Z0-9._-]+$ and cannot be empty or null or equal ak3-helper
if (moduleId.isEmpty() || moduleId == "ak3-helper" || !moduleId.matches(Regex("^[a-zA-Z][a-zA-Z0-9._-]+$"))) {
continue
}
// If module id start with a dot, warn user
if (moduleId[0] == '.') {
Timber.w("This is not recommended and may indicate an attempt to hide the module")
}
val moduleLastUpdate = module.getLong("last_update")
val moduleNotesUrl = module.getString("notes_url")
val modulePropsUrl = module.getString("prop_url")
val moduleZipUrl = module.getString("zip_url")
val moduleChecksum = module.optString("checksum")
val moduleStars = module.optString("stars")
var moduleDownloads = module.optString("downloads")
// if downloads is mull or empty, try to get it from the stats field
if (moduleDownloads.isEmpty() && module.has("stats")) {
moduleDownloads = module.optString("stats")
}
var repoModule = moduleHashMap[moduleId]
if (repoModule == null) {
repoModule = RepoModule(this, moduleId)
moduleHashMap[moduleId] = repoModule
newModules.add(repoModule)
} else {
if (repoModule.lastUpdated < moduleLastUpdate || repoModule.moduleInfo.hasFlag(
ModuleInfo.FLAG_METADATA_INVALID
)
) {
newModules.add(repoModule)
}
}
repoModule.processed = true
repoModule.repoName = nameForModules
repoModule.lastUpdated = moduleLastUpdate
repoModule.notesUrl = moduleNotesUrl
repoModule.propUrl = modulePropsUrl
repoModule.zipUrl = moduleZipUrl
repoModule.checksum = moduleChecksum
// safety check must be overridden per repo. only androidacy repo has this flag currently
// repoModule.safe = module.optBoolean("safe", false);
if (moduleStars.isNotEmpty()) {
try {
repoModule.qualityValue = moduleStars.toInt()
repoModule.qualityText = R.string.module_stars
} catch (ignored: NumberFormatException) {
}
} else if (moduleDownloads.isNotEmpty()) {
try {
repoModule.qualityValue = moduleDownloads.toInt()
repoModule.qualityText = R.string.module_downloads
} catch (ignored: NumberFormatException) {
}
}
}
// Remove no longer existing modules
val moduleInfoIterator = moduleHashMap.values.iterator()
while (moduleInfoIterator.hasNext()) {
val repoModule = moduleInfoIterator.next()
if (!repoModule.processed) {
val delete = File(cacheRoot, repoModule.id + ".prop").delete()
if (!delete) {
throw RuntimeException("Failed to delete module metadata")
}
moduleInfoIterator.remove()
} else {
repoModule.moduleInfo.verify()
}
}
// Update final metadata
this.name = name
this.lastUpdate = lastUpdate
website = jsonObject.optString("website")
support = jsonObject.optString("support")
donate = jsonObject.optString("donate")
submitModule = jsonObject.optString("submitModule")
}
return newModules
}
override val isEnabledByDefault: Boolean
get() = BuildConfig.ENABLED_REPOS.contains(preferenceId)
override var isEnabled: Boolean = false
get() = if (field) {
field
} else {
val realmConfiguration2 =
RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(
INSTANCE!!.key
).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(
INSTANCE!!.getDataDirWithPath("realms")
).schemaVersion(1).build()
val realm2 = Realm.getInstance(realmConfiguration2)
val dbEnabled = AtomicBoolean(false)
realm2.executeTransaction { realm: Realm ->
val reposList =
realm.where(ReposList::class.java).equalTo("id", preferenceId).findFirst()
if (reposList != null) {
dbEnabled.set(reposList.isEnabled)
} else {
// should never happen but for safety
dbEnabled.set(false)
}
}
realm2.close()
// should never happen but for safety
if (dbEnabled.get()) {
!isForceHide
} else {
false
}
}
set(value) {
field = value
this.enabled = enabled && !isForceHide
// reposlist realm
val realmConfiguration2 =
RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(
INSTANCE!!.key
).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(
INSTANCE!!.getDataDirWithPath("realms")
).schemaVersion(1).build()
val realm2 = Realm.getInstance(realmConfiguration2)
realm2.executeTransaction { realm: Realm ->
val reposList =
realm.where(ReposList::class.java).equalTo("id", preferenceId).findFirst()
if (reposList != null) {
reposList.isEnabled = enabled
}
}
realm2.close()
}
@Throws(IOException::class)
open fun storeMetadata(repoModule: RepoModule, data: ByteArray?) {
write(File(cacheRoot, repoModule.id + ".prop"), data)
}
open fun tryLoadMetadata(repoModule: RepoModule): Boolean {
val file = File(cacheRoot, repoModule.id + ".prop")
if (file.exists()) {
try {
val moduleInfo = repoModule.moduleInfo
readProperties(
moduleInfo,
file.absolutePath,
repoModule.repoName + "/" + moduleInfo.name,
false
)
moduleInfo.flags = moduleInfo.flags and ModuleInfo.FLAG_METADATA_INVALID.inv()
if (moduleInfo.version == null) {
moduleInfo.version = "v" + moduleInfo.versionCode
}
return true
} catch (ignored: Exception) {
val delete = file.delete()
if (!delete) {
throw RuntimeException("Failed to delete invalid metadata file")
}
}
} else {
Timber.d("Metadata file not found for %s", repoModule.id)
}
repoModule.moduleInfo.flags =
repoModule.moduleInfo.flags or ModuleInfo.FLAG_METADATA_INVALID
return false
}
fun updateEnabledState() {
// Make sure first_launch preference is set to false
if (MainActivity.doSetupNowRunning) {
return
}
if (preferenceId == null) {
Timber.e("Repo ID is null")
return
}
// if repo starts with repo_, it's always enabled bc custom repos can't be disabled without being deleted.
isForceHide = shouldForceHide(preferenceId!!)
// reposlist realm
val realmConfiguration2 =
RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(
INSTANCE!!.key
).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(
INSTANCE!!.getDataDirWithPath("realms")
).schemaVersion(1).build()
val realm2 = Realm.getInstance(realmConfiguration2)
var dbEnabled = false
try {
dbEnabled = realm2.where(
ReposList::class.java
).equalTo("id", preferenceId).findFirst()?.isEnabled == true
} catch (e: Exception) {
Timber.e(e, "Error while updating enabled state for repo %s", preferenceId)
}
realm2.close()
enabled = !isForceHide && dbEnabled
}
open fun getUrl(): String? {
return url
}
fun getWebsite(): String {
if (isNonNull(website)) return website!!
return if (defaultWebsite != null) defaultWebsite else url
}
fun getSupport(): String? {
return if (isNonNull(support)) support else defaultSupport
}
fun getDonate(): String? {
return if (isNonNull(donate)) donate else defaultDonate
}
fun getSubmitModule(): String? {
return if (isNonNull(submitModule)) submitModule else defaultSubmitModule
}
// should update (lastUpdate > 15 minutes)
fun shouldUpdate(): Boolean {
Timber.d("Repo $preferenceId should update check called")
val realmConfiguration2 =
RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(
INSTANCE!!.key
).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(
INSTANCE!!.getDataDirWithPath("realms")
).schemaVersion(1).build()
val realm2 = Realm.getInstance(realmConfiguration2)
val repo = realm2.where(ReposList::class.java).equalTo("id", preferenceId).findFirst()
// Make sure ModuleListCache for repoId is not null
val cacheRoot = INSTANCE!!.getDataDirWithPath("realms/repos/$preferenceId")
val realmConfiguration =
RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(
INSTANCE!!.key
).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true)
.allowQueriesOnUiThread(true).directory(cacheRoot).build()
val realm = Realm.getInstance(realmConfiguration)
val moduleListCache = realm.where(
ModuleListCache::class.java
).equalTo("repoId", preferenceId).findAll()
if (repo != null) {
return if (repo.lastUpdate != 0 && moduleListCache.size != 0) {
val lastUpdate = repo.lastUpdate.toLong()
val currentTime = System.currentTimeMillis()
val diff = currentTime - lastUpdate
val diffMinutes = diff / (60 * 1000) % 60
Timber.d("Repo $preferenceId updated: $diffMinutes minutes ago")
realm.close()
diffMinutes > if (BuildConfig.DEBUG) 15 else 30
} else {
Timber.d("Repo $preferenceId should update could not find repo in database")
Timber.d("This is probably an error, please report this to the developer")
realm.close()
true
}
} else {
realm.close()
}
return true
}
companion object {
private fun isNonNull(str: String?): Boolean {
return !str.isNullOrEmpty() && "null" != str
}
}
}

@ -1,401 +0,0 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.repo;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.fox2code.mmm.BuildConfig;
import com.fox2code.mmm.MainActivity;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.XHooks;
import com.fox2code.mmm.XRepo;
import com.fox2code.mmm.androidacy.AndroidacyRepoData;
import com.fox2code.mmm.manager.ModuleInfo;
import com.fox2code.mmm.utils.SyncManager;
import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.Hashes;
import com.fox2code.mmm.utils.io.PropUtils;
import com.fox2code.mmm.utils.io.net.Http;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import timber.log.Timber;
public final class RepoManager extends SyncManager {
public static final String MAGISK_REPO = "https://raw.githubusercontent.com/Magisk-Modules-Repo/submission/modules/modules.json";
public static final String MAGISK_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Repo";
public static final String MAGISK_ALT_REPO = "https://raw.githubusercontent.com/Magisk-Modules-Alt-Repo/json/main/modules.json";
public static final String MAGISK_ALT_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Alt-Repo";
public static final String MAGISK_ALT_REPO_JSDELIVR = "https://cdn.jsdelivr.net/gh/Magisk-Modules-Alt-Repo/json@main/modules.json";
public static final String ANDROIDACY_MAGISK_REPO_ENDPOINT = "https://production-api.androidacy.com/magisk/repo";
public static final String ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT = "https://staging-api.androidacy.com/magisk/repo";
public static final String ANDROIDACY_MAGISK_REPO_HOMEPAGE = "https://www.androidacy.com/modules-repo";
private static final String MAGISK_REPO_MANAGER = "https://magisk-modules-repo.github.io/submission/modules.json";
private static final Object lock = new Object();
private static final double STEP1 = 0.1D;
private static final double STEP2 = 0.8D;
private static final double STEP3 = 0.1D;
private static volatile RepoManager INSTANCE;
private final MainApplication mainApplication;
private final LinkedHashMap<String, RepoData> repoData;
private final HashMap<String, RepoModule> modules;
public String repoLastErrorName = null;
private AndroidacyRepoData androidacyRepoData;
private CustomRepoManager customRepoManager;
private boolean initialized;
private boolean repoLastSuccess;
private RepoManager(MainApplication mainApplication) {
INSTANCE = this; // Set early fox XHooks
this.initialized = false;
this.mainApplication = mainApplication;
this.repoData = new LinkedHashMap<>();
this.modules = new HashMap<>();
// refuse to load if setup is not complete
if (MainApplication.getSharedPreferences("mmm").getString("last_shown_setup", "").equals("")) {
return;
}
// We do not have repo list config yet.
this.androidacyRepoData = this.addAndroidacyRepoData();
RepoData altRepo = this.addRepoData(MAGISK_ALT_REPO, "Magisk Modules Alt Repo");
altRepo.defaultWebsite = RepoManager.MAGISK_ALT_REPO_HOMEPAGE;
altRepo.defaultSubmitModule = "https://github.com/Magisk-Modules-Alt-Repo/submission/issues";
this.customRepoManager = new CustomRepoManager(mainApplication, this);
XHooks.onRepoManagerInitialize();
// Populate default cache
boolean x = false;
for (RepoData repoData : this.repoData.values()) {
if (repoData == this.androidacyRepoData) {
if (x) return;
x = true;
}
this.populateDefaultCache(repoData);
}
this.initialized = true;
}
public static RepoManager getINSTANCE() {
if (INSTANCE == null || !INSTANCE.initialized) {
synchronized (lock) {
if (INSTANCE == null) {
MainApplication mainApplication = MainApplication.getINSTANCE();
if (mainApplication != null) {
INSTANCE = new RepoManager(mainApplication);
XHooks.onRepoManagerInitialized();
} else {
throw new RuntimeException("Getting RepoManager too soon!");
}
}
}
}
return INSTANCE;
}
public static RepoManager getINSTANCE_UNSAFE() {
if (INSTANCE == null) {
synchronized (lock) {
if (INSTANCE == null) {
MainApplication mainApplication = MainApplication.getINSTANCE();
if (mainApplication != null) {
INSTANCE = new RepoManager(mainApplication);
XHooks.onRepoManagerInitialized();
} else {
throw new RuntimeException("Getting RepoManager too soon!");
}
}
}
}
return INSTANCE;
}
public static String internalIdOfUrl(String url) {
return switch (url) {
case MAGISK_ALT_REPO, MAGISK_ALT_REPO_JSDELIVR -> "magisk_alt_repo";
case ANDROIDACY_MAGISK_REPO_ENDPOINT, ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT ->
"androidacy_repo";
default -> "repo_" + Hashes.hashSha256(url.getBytes(StandardCharsets.UTF_8));
};
}
static boolean isBuiltInRepo(String repo) {
return switch (repo) {
case RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT, RepoManager.ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT, RepoManager.MAGISK_ALT_REPO, RepoManager.MAGISK_ALT_REPO_JSDELIVR ->
true;
default -> false;
};
}
/**
* Safe way to do {@code RepoManager.getInstance().androidacyRepoData.isEnabled()}
* without initializing RepoManager
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isAndroidacyRepoEnabled() {
return INSTANCE != null && INSTANCE.androidacyRepoData != null && INSTANCE.androidacyRepoData.isEnabled();
}
@SuppressWarnings("StatementWithEmptyBody")
private void populateDefaultCache(RepoData repoData) {
// if last_shown_setup is not "v2", them=n refuse to continue
if (!MainApplication.getSharedPreferences("mmm").getString("last_shown_setup", "").equals("v2")) {
return;
}
// make sure repodata is not null
if (repoData == null || repoData.moduleHashMap == null) {
return;
}
for (RepoModule repoModule : repoData.moduleHashMap.values()) {
if (!repoModule.moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
RepoModule registeredRepoModule = this.modules.get(repoModule.id);
if (registeredRepoModule == null) {
this.modules.put(repoModule.id, repoModule);
} else if (AndroidacyRepoData.getInstance().isEnabled() && registeredRepoModule.repoData == this.androidacyRepoData) {
// empty
} else if (AndroidacyRepoData.getInstance().isEnabled() && repoModule.repoData == this.androidacyRepoData) {
this.modules.put(repoModule.id, repoModule);
} else if (repoModule.moduleInfo.versionCode > registeredRepoModule.moduleInfo.versionCode) {
this.modules.put(repoModule.id, repoModule);
}
} else {
Timber.e("Detected module with invalid metadata: " + repoModule.repoName + "/" + repoModule.id);
}
}
}
public RepoData get(String url) {
if (url == null) return null;
if (MAGISK_ALT_REPO_JSDELIVR.equals(url)) {
url = MAGISK_ALT_REPO;
}
return this.repoData.get(url);
}
public RepoData addOrGet(String url) {
return this.addOrGet(url, null);
}
public RepoData addOrGet(String url, String fallBackName) {
if (MAGISK_ALT_REPO_JSDELIVR.equals(url)) url = MAGISK_ALT_REPO;
RepoData repoData;
synchronized (this.syncLock) {
repoData = this.repoData.get(url);
if (repoData == null) {
if (ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT.equals(url) || ANDROIDACY_MAGISK_REPO_ENDPOINT.equals(url)) {
//noinspection ReplaceNullCheck
if (this.androidacyRepoData != null) {
return this.androidacyRepoData;
} else {
return this.addAndroidacyRepoData();
}
} else {
return this.addRepoData(url, fallBackName);
}
}
}
return repoData;
}
@SuppressWarnings("StatementWithEmptyBody")
@SuppressLint("StringFormatInvalid")
protected void scanInternal(@NonNull UpdateListener updateListener) {
// Refuse to start if first_launch is not false in shared preferences
if (MainActivity.doSetupNowRunning) {
return;
}
this.modules.clear();
updateListener.update(0D);
// Using LinkedHashSet to deduplicate Androidacy entry.
RepoData[] repoDatas = new LinkedHashSet<>(this.repoData.values()).toArray(new RepoData[0]);
RepoUpdater[] repoUpdaters = new RepoUpdater[repoDatas.length];
int moduleToUpdate = 0;
if (!this.hasConnectivity()) {
updateListener.update(STEP3);
return;
}
for (int i = 0; i < repoDatas.length; i++) {
if (BuildConfig.DEBUG) Timber.d("Preparing to fetch: %s", repoDatas[i].getName());
moduleToUpdate += (repoUpdaters[i] = new RepoUpdater(repoDatas[i])).fetchIndex();
updateListener.update(STEP1 / repoDatas.length * (i + 1));
}
if (BuildConfig.DEBUG) Timber.d("Updating meta-data");
int updatedModules = 0;
boolean allowLowQualityModules = MainApplication.Companion.isDisableLowQualityModuleFilter();
for (int i = 0; i < repoUpdaters.length; i++) {
// Check if the repo is enabled
if (!repoUpdaters[i].repoData.isEnabled()) {
if (BuildConfig.DEBUG)
Timber.d("Skipping disabled repo: %s", repoUpdaters[i].repoData.getName());
continue;
}
List<RepoModule> repoModules = repoUpdaters[i].toUpdate();
RepoData repoData = repoDatas[i];
if (BuildConfig.DEBUG) Timber.d("Registering %s", repoData.getName());
for (RepoModule repoModule : repoModules) {
try {
if (repoModule.propUrl != null && !repoModule.propUrl.isEmpty()) {
repoData.storeMetadata(repoModule, Http.doHttpGet(repoModule.propUrl, false));
Files.write(new File(repoData.cacheRoot, repoModule.id + ".prop"), Http.doHttpGet(repoModule.propUrl, false));
}
if (repoData.tryLoadMetadata(repoModule) && (allowLowQualityModules || !PropUtils.isLowQualityModule(repoModule.moduleInfo))) {
// Note: registeredRepoModule may not be null if registered by multiple repos
RepoModule registeredRepoModule = this.modules.get(repoModule.id);
if (registeredRepoModule == null) {
this.modules.put(repoModule.id, repoModule);
} else if (AndroidacyRepoData.getInstance().isEnabled() && registeredRepoModule.repoData == this.androidacyRepoData) {
// empty
} else if (AndroidacyRepoData.getInstance().isEnabled() && repoModule.repoData == this.androidacyRepoData) {
this.modules.put(repoModule.id, repoModule);
} else if (repoModule.moduleInfo.versionCode > registeredRepoModule.moduleInfo.versionCode) {
this.modules.put(repoModule.id, repoModule);
}
} else {
repoModule.moduleInfo.flags |= ModuleInfo.FLAG_METADATA_INVALID;
}
} catch (Exception e) {
Timber.e(e);
}
updatedModules++;
updateListener.update(STEP1 + (STEP2 / (moduleToUpdate != 0 ? moduleToUpdate : 1) * updatedModules));
}
for (RepoModule repoModule : repoUpdaters[i].toApply()) {
if ((repoModule.moduleInfo.flags & ModuleInfo.FLAG_METADATA_INVALID) == 0) {
RepoModule registeredRepoModule = this.modules.get(repoModule.id);
if (registeredRepoModule == null) {
this.modules.put(repoModule.id, repoModule);
} else if (AndroidacyRepoData.getInstance().isEnabled() && registeredRepoModule.repoData == this.androidacyRepoData) {
// empty
} else if (AndroidacyRepoData.getInstance().isEnabled() && repoModule.repoData == this.androidacyRepoData) {
this.modules.put(repoModule.id, repoModule);
} else if (repoModule.moduleInfo.versionCode > registeredRepoModule.moduleInfo.versionCode) {
this.modules.put(repoModule.id, repoModule);
}
}
}
}
if (BuildConfig.DEBUG) Timber.d("Finishing update");
if (hasConnectivity()) {
for (int i = 0; i < repoDatas.length; i++) {
// If repo is not enabled, skip
if (!repoDatas[i].isEnabled()) {
if (BuildConfig.DEBUG)
Timber.d("Skipping " + repoDatas[i].getName() + " because it's disabled");
continue;
}
if (BuildConfig.DEBUG)
Timber.d("Finishing: %s", repoUpdaters[i].repoData.getName());
this.repoLastSuccess = repoUpdaters[i].finish();
if (!this.repoLastSuccess) {
Timber.e("Failed to update %s", repoUpdaters[i].repoData.getName());
// Show snackbar on main looper and add some bottom padding
int finalI = i;
Activity context = MainApplication.getINSTANCE().getLastCompatActivity();
new Handler(Looper.getMainLooper()).post(() -> {
if (context != null) {
// Show material dialogue with the repo name. for androidacy repo, show an option to reset the api key. show a message then a list of errors
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
builder.setTitle(R.string.repo_update_failed);
builder.setMessage(context.getString(R.string.repo_update_failed_message, "- " + repoUpdaters[finalI].repoData.getName()));
builder.setPositiveButton(android.R.string.ok, null);
if (repoUpdaters[finalI].repoData instanceof AndroidacyRepoData) {
builder.setNeutralButton(R.string.reset_api_key, (dialog, which) -> {
SharedPreferences.Editor editor = MainApplication.getSharedPreferences("androidacy").edit();
editor.putString("androidacy_api_key", "");
editor.apply();
Toast.makeText(context, R.string.api_key_removed, Toast.LENGTH_SHORT).show();
});
}
builder.show();
}
});
this.repoLastErrorName = repoUpdaters[i].repoData.getName();
}
updateListener.update(STEP1 + STEP2 + (STEP3 / repoDatas.length * (i + 1)));
}
}
Timber.i("Got " + this.modules.size() + " modules!");
updateListener.update(1D);
}
public void updateEnabledStates() {
for (RepoData repoData : this.repoData.values()) {
boolean wasEnabled = repoData.isEnabled();
repoData.updateEnabledState();
if (!wasEnabled && repoData.isEnabled()) {
this.customRepoManager.dirty = true;
}
}
}
public HashMap<String, RepoModule> getModules() {
this.afterUpdate();
return this.modules;
}
public boolean hasConnectivity() {
return Http.hasConnectivity(MainApplication.getINSTANCE().getApplicationContext());
}
private RepoData addRepoData(String url, String fallBackName) {
String id = internalIdOfUrl(url);
File cacheRoot = new File(this.mainApplication.getDataDir(), "repos/" + id);
RepoData repoData = id.startsWith("repo_") ? new CustomRepoData(url, cacheRoot) : new RepoData(url, cacheRoot);
if (fallBackName != null && !fallBackName.isEmpty()) {
repoData.defaultName = fallBackName;
if (repoData instanceof CustomRepoData) {
((CustomRepoData) repoData).loadedExternal = true;
this.customRepoManager.dirty = true;
repoData.updateEnabledState();
}
}
switch (url) {
case MAGISK_REPO, MAGISK_REPO_MANAGER -> repoData.defaultWebsite = MAGISK_REPO_HOMEPAGE;
}
this.repoData.put(url, repoData);
if (this.initialized) {
this.populateDefaultCache(repoData);
}
return repoData;
}
private AndroidacyRepoData addAndroidacyRepoData() {
// cache dir is actually under app data
File cacheRoot = this.mainApplication.getDataDirWithPath("realms/repos/androidacy_repo");
AndroidacyRepoData repoData = new AndroidacyRepoData(cacheRoot, MainApplication.Companion.isAndroidacyTestMode());
this.repoData.put(ANDROIDACY_MAGISK_REPO_ENDPOINT, repoData);
this.repoData.put(ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT, repoData);
return repoData;
}
public AndroidacyRepoData getAndroidacyRepoData() {
return this.androidacyRepoData;
}
public CustomRepoManager getCustomRepoManager() {
return customRepoManager;
}
public Collection<XRepo> getXRepos() {
return new LinkedHashSet<>(this.repoData.values());
}
public boolean isLastUpdateSuccess() {
return this.repoLastSuccess;
}
}

@ -0,0 +1,411 @@
/*
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
*/
package com.fox2code.mmm.repo
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import com.fox2code.mmm.BuildConfig
import com.fox2code.mmm.MainActivity
import com.fox2code.mmm.MainApplication
import com.fox2code.mmm.MainApplication.Companion.getSharedPreferences
import com.fox2code.mmm.MainApplication.Companion.isAndroidacyTestMode
import com.fox2code.mmm.MainApplication.Companion.isDisableLowQualityModuleFilter
import com.fox2code.mmm.R
import com.fox2code.mmm.XHooks.Companion.onRepoManagerInitialize
import com.fox2code.mmm.XHooks.Companion.onRepoManagerInitialized
import com.fox2code.mmm.XRepo
import com.fox2code.mmm.androidacy.AndroidacyRepoData
import com.fox2code.mmm.androidacy.AndroidacyRepoData.Companion.instance
import com.fox2code.mmm.manager.ModuleInfo
import com.fox2code.mmm.utils.SyncManager
import com.fox2code.mmm.utils.io.Files.Companion.write
import com.fox2code.mmm.utils.io.Hashes.Companion.hashSha256
import com.fox2code.mmm.utils.io.PropUtils.Companion.isLowQualityModule
import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet
import com.fox2code.mmm.utils.io.net.Http.Companion.hasConnectivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import timber.log.Timber
import java.io.File
import java.nio.charset.StandardCharsets
@Suppress("NAME_SHADOWING", "unused")
class RepoManager private constructor(mainApplication: MainApplication) : SyncManager() {
private val mainApplication: MainApplication
private val repoData: LinkedHashMap<String?, RepoData>
val modules: HashMap<String, RepoModule>
get() {
afterUpdate()
return field
}
private var repoLastErrorName: String? = null
var androidacyRepoData: AndroidacyRepoData? = null
var customRepoManager: CustomRepoManager? = null
private var initialized: Boolean
var isLastUpdateSuccess = false
private set
init {
INSTANCE = this // Set early fox XHooks
initialized = false
this.mainApplication = mainApplication
repoData = LinkedHashMap()
modules = HashMap()
// refuse to load if setup is not complete
if (getSharedPreferences("mmm")!!.getString("last_shown_setup", "") != "") {
// We do not have repo list config yet.
androidacyRepoData = addAndroidacyRepoData()
val altRepo = addRepoData(MAGISK_ALT_REPO, "Magisk Modules Alt Repo")
altRepo.defaultWebsite = MAGISK_ALT_REPO_HOMEPAGE
altRepo.defaultSubmitModule =
"https://github.com/Magisk-Modules-Alt-Repo/submission/issues"
customRepoManager = CustomRepoManager(mainApplication, this)
onRepoManagerInitialize()
// Populate default cache
var x = false
for (repoData in repoData.values) {
if (repoData === androidacyRepoData) {
if (x) {
Timber.e("Multiple Androidacy repo detected")
} else {
x = true
}
}
populateDefaultCache(repoData)
}
initialized = true
}
}
private fun populateDefaultCache(repoData: RepoData?) {
// if last_shown_setup is not "v2", them=n refuse to continue
if (getSharedPreferences("mmm")!!.getString("last_shown_setup", "") != "v2") {
return
}
// make sure repodata is not null
if (repoData?.moduleHashMap == null) {
return
}
for (repoModule in repoData.moduleHashMap.values) {
if (!repoModule.moduleInfo.hasFlag(ModuleInfo.FLAG_METADATA_INVALID)) {
val registeredRepoModule = modules[repoModule.id]
if (registeredRepoModule == null) {
modules[repoModule.id] = repoModule
} else if (instance.isEnabled && registeredRepoModule.repoData === androidacyRepoData) {
// empty
} else if (instance.isEnabled && repoModule.repoData === androidacyRepoData) {
modules[repoModule.id] = repoModule
} else if (repoModule.moduleInfo.versionCode > registeredRepoModule.moduleInfo.versionCode) {
modules[repoModule.id] = repoModule
}
} else {
Timber.e("Detected module with invalid metadata: " + repoModule.repoName + "/" + repoModule.id)
}
}
}
operator fun get(url: String?): RepoData? {
var url = url ?: return null
if (MAGISK_ALT_REPO_JSDELIVR == url) {
url = MAGISK_ALT_REPO
}
return repoData[url]
}
@JvmOverloads
fun addOrGet(url: String, fallBackName: String? = null): RepoData? {
var url = url
if (MAGISK_ALT_REPO_JSDELIVR == url) url = MAGISK_ALT_REPO
var repoData: RepoData?
synchronized(syncLock) {
repoData = this.repoData[url]
if (repoData == null) {
return if (ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT == url || ANDROIDACY_MAGISK_REPO_ENDPOINT == url) {
androidacyRepoData ?: addAndroidacyRepoData()
} else {
addRepoData(url, fallBackName)
}
}
}
return repoData
}
@SuppressLint("StringFormatInvalid")
override fun scanInternal(updateListener: UpdateListener) {
// Refuse to start if first_launch is not false in shared preferences
if (MainActivity.doSetupNowRunning) {
return
}
modules.clear()
updateListener.update(0.0)
// Using LinkedHashSet to deduplicate Androidacy entry.
val repoDatas = LinkedHashSet(repoData.values).toTypedArray()
val repoUpdaters = arrayOfNulls<RepoUpdater>(repoDatas.size)
var moduleToUpdate = 0
if (!this.hasConnectivity()) {
updateListener.update(STEP3)
return
}
for (i in repoDatas.indices) {
if (BuildConfig.DEBUG) Timber.d("Preparing to fetch: %s", repoDatas[i].name)
moduleToUpdate += RepoUpdater(repoDatas[i]).also { repoUpdaters[i] = it }.fetchIndex()
updateListener.update(STEP1 / repoDatas.size * (i + 1))
}
if (BuildConfig.DEBUG) Timber.d("Updating meta-data")
var updatedModules = 0
val allowLowQualityModules = isDisableLowQualityModuleFilter
for (i in repoUpdaters.indices) {
// Check if the repo is enabled
if (!repoUpdaters[i]!!.repoData.isEnabled) {
if (BuildConfig.DEBUG) Timber.d(
"Skipping disabled repo: %s",
repoUpdaters[i]!!.repoData.name
)
continue
}
val repoModules = repoUpdaters[i]!!.toUpdate()
val repoData = repoDatas[i]
if (BuildConfig.DEBUG) Timber.d("Registering %s", repoData.name)
for (repoModule in repoModules!!) {
try {
if (repoModule.propUrl != null && repoModule.propUrl!!.isNotEmpty()) {
repoData.storeMetadata(
repoModule, doHttpGet(
repoModule.propUrl!!, false
)
)
write(
File(repoData.cacheRoot, repoModule.id + ".prop"), doHttpGet(
repoModule.propUrl!!, false
)
)
}
if (repoData.tryLoadMetadata(repoModule) && (allowLowQualityModules || !isLowQualityModule(
repoModule.moduleInfo
))
) {
// Note: registeredRepoModule may not be null if registered by multiple repos
val registeredRepoModule = modules[repoModule.id]
if (registeredRepoModule == null) {
modules[repoModule.id] = repoModule
} else if (instance.isEnabled && registeredRepoModule.repoData === androidacyRepoData) {
// empty
} else if (instance.isEnabled && repoModule.repoData === androidacyRepoData) {
modules[repoModule.id] = repoModule
} else if (repoModule.moduleInfo.versionCode > registeredRepoModule.moduleInfo.versionCode) {
modules[repoModule.id] = repoModule
}
} else {
repoModule.moduleInfo.flags =
repoModule.moduleInfo.flags or ModuleInfo.FLAG_METADATA_INVALID
}
} catch (e: Exception) {
Timber.e(e)
}
updatedModules++
updateListener.update(STEP1 + STEP2 / (if (moduleToUpdate != 0) moduleToUpdate else 1) * updatedModules)
}
for (repoModule in repoUpdaters[i]!!.toApply()!!) {
if (repoModule.moduleInfo.flags and ModuleInfo.FLAG_METADATA_INVALID == 0) {
val registeredRepoModule = modules[repoModule.id]
if (registeredRepoModule == null) {
modules[repoModule.id] = repoModule
} else if (instance.isEnabled && registeredRepoModule.repoData === androidacyRepoData) {
// empty
} else if (instance.isEnabled && repoModule.repoData === androidacyRepoData) {
modules[repoModule.id] = repoModule
} else if (repoModule.moduleInfo.versionCode > registeredRepoModule.moduleInfo.versionCode) {
modules[repoModule.id] = repoModule
}
}
}
}
if (BuildConfig.DEBUG) Timber.d("Finishing update")
if (hasConnectivity()) {
for (i in repoDatas.indices) {
// If repo is not enabled, skip
if (!repoDatas[i].isEnabled) {
if (BuildConfig.DEBUG) Timber.d("Skipping " + repoDatas[i].name + " because it's disabled")
continue
}
if (BuildConfig.DEBUG) Timber.d("Finishing: %s", repoUpdaters[i]!!.repoData.name)
isLastUpdateSuccess = repoUpdaters[i]!!.finish()
if (!isLastUpdateSuccess) {
Timber.e("Failed to update %s", repoUpdaters[i]!!.repoData.name)
// Show snackbar on main looper and add some bottom padding
val context: Activity? = MainApplication.INSTANCE!!.lastCompatActivity
Handler(Looper.getMainLooper()).post {
if (context != null) {
// Show material dialogue with the repo name. for androidacy repo, show an option to reset the api key. show a message then a list of errors
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(R.string.repo_update_failed)
builder.setMessage(
context.getString(
R.string.repo_update_failed_message,
"- " + repoUpdaters[i]!!.repoData.name
)
)
builder.setPositiveButton(android.R.string.ok, null)
if (repoUpdaters[i]!!.repoData is AndroidacyRepoData) {
builder.setNeutralButton(R.string.reset_api_key) { _: DialogInterface?, _: Int ->
val editor = getSharedPreferences("androidacy")!!
.edit()
editor.putString("androidacy_api_key", "")
editor.apply()
Toast.makeText(
context,
R.string.api_key_removed,
Toast.LENGTH_SHORT
).show()
}
}
builder.show()
}
}
repoLastErrorName = repoUpdaters[i]!!.repoData.name
}
updateListener.update(STEP1 + STEP2 + STEP3 / repoDatas.size * (i + 1))
}
}
Timber.i("Got " + modules.size + " modules!")
updateListener.update(1.0)
}
fun updateEnabledStates() {
for (repoData in repoData.values) {
val wasEnabled = repoData.isEnabled
repoData.updateEnabledState()
if (!wasEnabled && repoData.isEnabled) {
customRepoManager!!.dirty = true
}
}
}
fun hasConnectivity(): Boolean {
return hasConnectivity(MainApplication.INSTANCE!!.applicationContext)
}
private fun addRepoData(url: String, fallBackName: String?): RepoData {
val id = internalIdOfUrl(url)
val cacheRoot = File(mainApplication.dataDir, "repos/$id")
val repoData =
if (id.startsWith("repo_")) CustomRepoData(url, cacheRoot) else RepoData(url, cacheRoot)
if (!fallBackName.isNullOrEmpty()) {
repoData.defaultName = fallBackName
if (repoData is CustomRepoData) {
repoData.loadedExternal = true
customRepoManager!!.dirty = true
repoData.updateEnabledState()
}
}
when (url) {
MAGISK_REPO, MAGISK_REPO_MANAGER -> repoData.defaultWebsite = MAGISK_REPO_HOMEPAGE
}
this.repoData[url] = repoData
if (initialized) {
populateDefaultCache(repoData)
}
return repoData
}
private fun addAndroidacyRepoData(): AndroidacyRepoData {
// cache dir is actually under app data
val cacheRoot = mainApplication.getDataDirWithPath("realms/repos/androidacy_repo")
val repoData = AndroidacyRepoData(cacheRoot, isAndroidacyTestMode)
this.repoData[ANDROIDACY_MAGISK_REPO_ENDPOINT] =
repoData
this.repoData[ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT] = repoData
return repoData
}
val xRepos: Collection<XRepo>
get() = LinkedHashSet<XRepo>(repoData.values)
companion object {
const val MAGISK_REPO =
"https://raw.githubusercontent.com/Magisk-Modules-Repo/submission/modules/modules.json"
const val MAGISK_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Repo"
const val MAGISK_ALT_REPO =
"https://raw.githubusercontent.com/Magisk-Modules-Alt-Repo/json/main/modules.json"
const val MAGISK_ALT_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Alt-Repo"
const val MAGISK_ALT_REPO_JSDELIVR =
"https://cdn.jsdelivr.net/gh/Magisk-Modules-Alt-Repo/json@main/modules.json"
const val ANDROIDACY_MAGISK_REPO_ENDPOINT =
"https://production-api.androidacy.com/magisk/repo"
const val ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT =
"https://staging-api.androidacy.com/magisk/repo"
const val ANDROIDACY_MAGISK_REPO_HOMEPAGE = "https://www.androidacy.com/modules-repo"
private const val MAGISK_REPO_MANAGER =
"https://magisk-modules-repo.github.io/submission/modules.json"
private val lock = Any()
private const val STEP1 = 0.1
private const val STEP2 = 0.8
private const val STEP3 = 0.1
@Volatile
private var INSTANCE: RepoManager? = null
@JvmStatic
fun getINSTANCE(): RepoManager? {
if (INSTANCE == null || !INSTANCE!!.initialized) {
synchronized(lock) {
if (INSTANCE == null) {
val mainApplication = MainApplication.INSTANCE
if (mainApplication != null) {
INSTANCE = RepoManager(mainApplication)
onRepoManagerInitialized()
} else {
throw RuntimeException("Getting RepoManager too soon!")
}
}
}
}
return INSTANCE
}
val iNSTANCE_UNSAFE: RepoManager?
get() {
if (INSTANCE == null) {
synchronized(lock) {
if (INSTANCE == null) {
val mainApplication = MainApplication.INSTANCE
if (mainApplication != null) {
INSTANCE = RepoManager(mainApplication)
onRepoManagerInitialized()
} else {
throw RuntimeException("Getting RepoManager too soon!")
}
}
}
}
return INSTANCE
}
@JvmStatic
fun internalIdOfUrl(url: String): String {
return when (url) {
MAGISK_ALT_REPO, MAGISK_ALT_REPO_JSDELIVR -> "magisk_alt_repo"
ANDROIDACY_MAGISK_REPO_ENDPOINT, ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT -> "androidacy_repo"
else -> "repo_" + hashSha256(url.toByteArray(StandardCharsets.UTF_8))
}
}
fun isBuiltInRepo(repo: String?): Boolean {
return when (repo) {
ANDROIDACY_MAGISK_REPO_ENDPOINT, ANDROIDACY_TEST_MAGISK_REPO_ENDPOINT, MAGISK_ALT_REPO, MAGISK_ALT_REPO_JSDELIVR -> true
else -> false
}
}
/**
* Safe way to do `RepoManager.getINSTANCE()!!.androidacyRepoData.isEnabled()`
* without initializing RepoManager
*/
val isAndroidacyRepoEnabled: Boolean
get() = INSTANCE != null && INSTANCE!!.androidacyRepoData != null && INSTANCE!!.androidacyRepoData!!.isEnabled
}
}

@ -23,7 +23,7 @@ class RepoUpdater(repoData2: RepoData) {
private var toUpdate: List<RepoModule>? = null
private var toApply: Collection<RepoModule>? = null
fun fetchIndex(): Int {
if (!RepoManager.getINSTANCE().hasConnectivity()) {
if (!RepoManager.getINSTANCE()!!.hasConnectivity()) {
indexRaw = null
toUpdate = emptyList()
toApply = emptySet()
@ -36,10 +36,10 @@ class RepoUpdater(repoData2: RepoData) {
return 0
}
// if we shouldn't update, get the values from the ModuleListCache realm
if (!repoData.shouldUpdate() && repoData.id == "androidacy_repo") { // for now, only enable cache reading for androidacy repo, until we handle storing module prop file values in cache
Timber.d("Fetching index from cache for %s", repoData.id)
if (!repoData.shouldUpdate() && repoData.preferenceId == "androidacy_repo") { // for now, only enable cache reading for androidacy repo, until we handle storing module prop file values in cache
Timber.d("Fetching index from cache for %s", repoData.preferenceId)
val cacheRoot =
MainApplication.INSTANCE!!.getDataDirWithPath("realms/repos/" + repoData.id)
MainApplication.INSTANCE!!.getDataDirWithPath("realms/repos/" + repoData.preferenceId)
val realmConfiguration = RealmConfiguration.Builder().name("ModuleListCache.realm")
.encryptionKey(MainApplication.INSTANCE!!.key).schemaVersion(1)
.deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true)
@ -47,7 +47,7 @@ class RepoUpdater(repoData2: RepoData) {
val realm = Realm.getInstance(realmConfiguration)
val results = realm.where(
ModuleListCache::class.java
).equalTo("repoId", repoData.id).findAll()
).equalTo("repoId", repoData.preferenceId).findAll()
// repos-list realm
val realmConfiguration2 = RealmConfiguration.Builder().name("ReposList.realm")
.encryptionKey(MainApplication.INSTANCE!!.key).allowQueriesOnUiThread(true)
@ -76,7 +76,7 @@ class RepoUpdater(repoData2: RepoData) {
Timber.d(
"Fetched %d modules from cache for %s, from %s records",
(toApply as HashSet<RepoModule>).size,
repoData.id,
repoData.preferenceId,
results.size
)
// apply the toApply list to the toUpdate list
@ -103,7 +103,7 @@ class RepoUpdater(repoData2: RepoData) {
toApply = repoData.moduleHashMap.values
return 0
}
indexRaw = doHttpGet(repoData.getUrl(), false)
indexRaw = repoData.getUrl()?.let { doHttpGet(it, false) }
toUpdate = repoData.populate(JSONObject(String(indexRaw!!, StandardCharsets.UTF_8)))
// Since we reuse instances this should work
toApply = HashSet(repoData.moduleHashMap.values)
@ -143,7 +143,7 @@ class RepoUpdater(repoData2: RepoData) {
// use realm to insert to
// props avail:
val cacheRoot =
MainApplication.INSTANCE!!.getDataDirWithPath("realms/repos/" + repoData.id)
MainApplication.INSTANCE!!.getDataDirWithPath("realms/repos/" + repoData.preferenceId)
val realmConfiguration = RealmConfiguration.Builder().name("ModuleListCache.realm")
.encryptionKey(MainApplication.INSTANCE!!.key).schemaVersion(1)
.deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true)
@ -187,7 +187,7 @@ class RepoUpdater(repoData2: RepoData) {
realm.commitTransaction()
}
realm.beginTransaction()
realm.where(ModuleListCache::class.java).equalTo("repoId", repoData.id).findAll()
realm.where(ModuleListCache::class.java).equalTo("repoId", repoData.preferenceId).findAll()
.deleteAllFromRealm()
realm.commitTransaction()
// iterate over modules. pls don't hate me for this, its ugly but it works
@ -289,7 +289,7 @@ class RepoUpdater(repoData2: RepoData) {
0
}
// get module repo id
val repoId = repoData.id
val repoId = repoData.preferenceId
// get module installed
val installed = false
// get module installed version code
@ -361,20 +361,20 @@ class RepoUpdater(repoData2: RepoData) {
// set lastUpdate
realm2.executeTransaction { r: Realm ->
val repoListCache =
r.where(ReposList::class.java).equalTo("id", repoData.id).findFirst()
r.where(ReposList::class.java).equalTo("id", repoData.preferenceId).findFirst()
if (repoListCache != null) {
success.set(true)
// get unix timestamp of current time
val currentTime = (System.currentTimeMillis() / 1000).toInt()
Timber.d(
"Updating lastUpdate for repo %s to %s which is %s seconds ago",
repoData.id,
repoData.preferenceId,
currentTime,
currentTime - repoListCache.lastUpdate
)
repoListCache.lastUpdate = currentTime
} else {
Timber.w("Failed to update lastUpdate for repo %s", repoData.id)
Timber.w("Failed to update lastUpdate for repo %s", repoData.preferenceId)
}
}
realm2.close()

@ -1320,7 +1320,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
realm.commitTransaction();
}
realm.beginTransaction();
Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", repoData.id).findFirst()).deleteFromRealm();
Objects.requireNonNull(realm.where(ReposList.class).equalTo("id", repoData.preferenceId).findFirst()).deleteFromRealm();
realm.commitTransaction();
customRepoManager.removeRepo(index);
updateCustomRepoList(false);
@ -1447,7 +1447,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
private void setRepoData(String url) {
final RepoData repoData = RepoManager.getINSTANCE().get(url);
setRepoData(repoData, "pref_" + (repoData == null ? RepoManager.internalIdOfUrl(url) : repoData.getPreferenceId()));
setRepoData(repoData, "pref_" + (repoData == null ? RepoManager.internalIdOfUrl(url) : repoData.preferenceId));
}
private void setRepoData(final RepoData repoData, String preferenceName) {
@ -1460,14 +1460,13 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
if (repoData != null) {
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build();
Realm realm = Realm.getInstance(realmConfiguration);
RealmResults<ReposList> repoDataRealmResults = realm.where(ReposList.class).equalTo("id", repoData.id).findAll();
RealmResults<ReposList> repoDataRealmResults = realm.where(ReposList.class).equalTo("id", repoData.preferenceId).findAll();
Timber.d("Setting preference " + preferenceName + " because it is not the Androidacy repo or the Magisk Alt Repo");
if (repoData.isForceHide() || repoDataRealmResults.isEmpty()) {
Timber.d("Hiding preference " + preferenceName + " because it is null or force hidden");
hideRepoData(preferenceName);
return;
} else {
//noinspection ConstantConditions
Timber.d("Showing preference %s because the forceHide status is %s and the RealmResults is %s", preferenceName, repoData.isForceHide(), repoDataRealmResults.toString());
preference.setTitle(repoData.getName());
preference.setVisible(true);

@ -7,7 +7,7 @@
<!-- redesign name -->
<string name="app_name_v2">Androidacy Module Manager</string>
<string name="app_name_short">Fox\'s Mmm</string>
<string name="app_name_short_v2">AndroidacyMM</string>
<string name="app_name_short_v2">AMM</string>
<string name="fail_root_magisk">Could not access either Root or Magisk</string>
<string name="fail_root_denied">Root has been denied via the Magisk app</string>
<string name="fail_magisk_missing">Magisk is not installed on this device</string>

Loading…
Cancel
Save