You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
FoxMagiskModuleManager/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java

365 lines
18 KiB
Java

package com.fox2code.mmm.repo;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.utils.io.net.Http;
import com.fox2code.mmm.utils.realm.ModuleListCache;
import com.fox2code.mmm.utils.realm.ReposList;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
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 RepoUpdater {
public final RepoData repoData;
public byte[] indexRaw;
private List<RepoModule> toUpdate;
private Collection<RepoModule> toApply;
public RepoUpdater(RepoData repoData) {
this.repoData = repoData;
}
public int fetchIndex() {
if (!RepoManager.getINSTANCE().hasConnectivity()) {
this.indexRaw = null;
this.toUpdate = Collections.emptyList();
this.toApply = Collections.emptySet();
return 0;
}
if (!this.repoData.isEnabled()) {
this.indexRaw = null;
this.toUpdate = Collections.emptyList();
this.toApply = Collections.emptySet();
return 0;
}
// if we shouldn't update, get the values from the ModuleListCache realm
if (!this.repoData.shouldUpdate() && Objects.equals(this.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", this.repoData.id);
File cacheRoot = MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/" + this.repoData.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> results = realm.where(ModuleListCache.class).equalTo("repoId", this.repoData.id).findAll();
// repos-list 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);
this.toUpdate = Collections.emptyList();
this.toApply = new HashSet<>();
for (ModuleListCache moduleListCache : results) {
this.toApply.add(new RepoModule(this.repoData, moduleListCache.getCodename(), moduleListCache.getName(), moduleListCache.getDescription(), moduleListCache.getAuthor(), moduleListCache.getDonate(), moduleListCache.getConfig(), moduleListCache.getSupport(), moduleListCache.getVersion(), moduleListCache.getVersionCode()));
}
Timber.d("Fetched %d modules from cache for %s, from %s records", this.toApply.size(), this.repoData.id, results.size());
// apply the toApply list to the toUpdate list
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("modules", new JSONArray(results.asJSON()));
this.toUpdate = this.repoData.populate(jsonObject);
} catch (Exception e) {
Timber.e(e);
}
// close realm
realm.close();
realm2.close();
// Since we reuse instances this should work
this.toApply = new HashSet<>(this.repoData.moduleHashMap.values());
this.toApply.removeAll(this.toUpdate);
// Return repo to update
return this.toUpdate.size();
}
try {
if (!this.repoData.prepare()) {
this.indexRaw = null;
this.toUpdate = Collections.emptyList();
this.toApply = this.repoData.moduleHashMap.values();
return 0;
}
this.indexRaw = Http.doHttpGet(this.repoData.getUrl(), false);
this.toUpdate = this.repoData.populate(new JSONObject(new String(this.indexRaw, StandardCharsets.UTF_8)));
// Since we reuse instances this should work
this.toApply = new HashSet<>(this.repoData.moduleHashMap.values());
this.toApply.removeAll(this.toUpdate);
// Return repo to update
return this.toUpdate.size();
} catch (
Exception e) {
Timber.e(e);
this.indexRaw = null;
this.toUpdate = Collections.emptyList();
this.toApply = Collections.emptySet();
return 0;
}
}
public List<RepoModule> toUpdate() {
return this.toUpdate;
}
public Collection<RepoModule> toApply() {
return this.toApply;
}
public boolean finish() {
var success = new AtomicBoolean(false);
// If repo is not enabled we don't need to do anything, just return true
if (!this.repoData.isEnabled()) {
return true;
}
if (this.indexRaw != null) {
try {
// iterate over modules, using this.supportedProperties as a template to attempt to get each property from the module. everything that is not null is added to the module
// use realm to insert to
// props avail:
File cacheRoot = MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/" + this.repoData.id);
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build();
// 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)
//
// all except first six can be null
// this.indexRaw is the raw index file (json)
JSONObject modules = new JSONObject(new String(this.indexRaw, StandardCharsets.UTF_8));
JSONArray modulesArray;
// androidacy repo uses "data" key, others should use "modules" key. Both are JSONArrays
if (this.repoData.getName().equals("Androidacy Modules Repo")) {
// get modules from "data" key. This is a JSONArray so we need to convert it to a JSONObject
modulesArray = modules.getJSONArray("data");
} else {
// get modules from "modules" key. This is a JSONArray so we need to convert it to a JSONObject
modulesArray = modules.getJSONArray("modules");
}
Realm realm = Realm.getInstance(realmConfiguration);
// drop old data
if (realm.isInTransaction()) {
realm.commitTransaction();
}
realm.beginTransaction();
realm.where(ModuleListCache.class).equalTo("repoId", this.repoData.id).findAll().deleteAllFromRealm();
realm.commitTransaction();
// iterate over modules. pls don't hate me for this, its ugly but it works
for (int n = 0; n < modulesArray.length(); n++) {
// get module
JSONObject module = modulesArray.getJSONObject(n);
try {
// get module id
// if codename is present, prefer that over id
String id;
if (module.has("codename") && !module.getString("codename").equals("")) {
id = module.getString("codename");
} else {
id = module.getString("id");
}
// get module name
String name = module.getString("name");
// get module version
String version = module.getString("version");
// get module version code
int versionCode = module.getInt("versionCode");
// get module author
String author = module.getString("author");
// get module description
String description = module.getString("description");
// get module min api
String minApi;
if (module.has("minApi") && !module.getString("minApi").equals("")) {
minApi = module.getString("minApi");
} else {
minApi = "0";
}
// coerce min api to int
int minApiInt = Integer.parseInt(minApi);
// get module max api and set to 0 if it's "" or null
String maxApi;
if (module.has("maxApi") && !module.getString("maxApi").equals("")) {
maxApi = module.getString("maxApi");
} else {
maxApi = "0";
}
// coerce max api to int
int maxApiInt = Integer.parseInt(maxApi);
// get module min magisk
String minMagisk;
if (module.has("minMagisk") && !module.getString("minMagisk").equals("")) {
minMagisk = module.getString("minMagisk");
} else {
minMagisk = "0";
}
// coerce min magisk to int
int minMagiskInt = Integer.parseInt(minMagisk);
// get module need ramdisk
boolean needRamdisk;
if (module.has("needRamdisk")) {
needRamdisk = module.getBoolean("needRamdisk");
} else {
needRamdisk = false;
}
// get module support
String support;
if (module.has("support")) {
support = module.getString("support");
} else {
support = "";
}
// get module donate
String donate;
if (module.has("donate")) {
donate = module.getString("donate");
} else {
donate = "";
}
// get module config
String config;
if (module.has("config")) {
config = module.getString("config");
} else {
config = "";
}
// get module change boot
boolean changeBoot;
if (module.has("changeBoot")) {
changeBoot = module.getBoolean("changeBoot");
} else {
changeBoot = false;
}
// get module mmt reborn
boolean mmtReborn;
if (module.has("mmtReborn")) {
mmtReborn = module.getBoolean("mmtReborn");
} else {
mmtReborn = false;
}
// try to get updated_at or lastUpdate value for lastUpdate
int lastUpdate;
if (module.has("updated_at")) {
lastUpdate = module.getInt("updated_at");
} else if (module.has("lastUpdate")) {
lastUpdate = module.getInt("lastUpdate");
} else {
lastUpdate = 0;
}
// now downloads or stars
int downloads;
if (module.has("downloads")) {
downloads = module.getInt("downloads");
} else if (module.has("stars")) {
downloads = module.getInt("stars");
} else {
downloads = 0;
}
// get module repo id
String repoId = this.repoData.id;
// get module installed
boolean installed = false;
// get module installed version code
int installedVersionCode = 0;
// get safe property. for now, only supported by androidacy repo and they use "vt_status" key
boolean safe = false;
if (this.repoData.getName().equals("Androidacy Modules Repo")) {
if (module.has("vt_status")) {
if (module.getString("vt_status").equals("Clean")) {
safe = true;
}
}
}
// insert module to realm
// first create a collection of all the properties
// then insert to realm
// then commit
// then close
if (realm.isInTransaction()) {
realm.cancelTransaction();
}
// create a realm object and insert or update it
// add everything to the realm object
if (realm.isInTransaction()) {
realm.commitTransaction();
}
realm.beginTransaction();
ModuleListCache moduleListCache = realm.createObject(ModuleListCache.class, id);
moduleListCache.setName(name);
moduleListCache.setVersion(version);
moduleListCache.setVersionCode(versionCode);
moduleListCache.setAuthor(author);
moduleListCache.setDescription(description);
moduleListCache.setMinApi(minApiInt);
moduleListCache.setMaxApi(maxApiInt);
moduleListCache.setMinMagisk(minMagiskInt);
moduleListCache.setNeedRamdisk(needRamdisk);
moduleListCache.setSupport(support);
moduleListCache.setDonate(donate);
moduleListCache.setConfig(config);
moduleListCache.setChangeBoot(changeBoot);
moduleListCache.setMmtReborn(mmtReborn);
moduleListCache.setRepoId(repoId);
moduleListCache.setInstalled(installed);
moduleListCache.setInstalledVersionCode(installedVersionCode);
moduleListCache.setSafe(safe);
moduleListCache.setLastUpdate(lastUpdate);
moduleListCache.setStats(downloads);
realm.copyToRealmOrUpdate(moduleListCache);
realm.commitTransaction();
} catch (
Exception ignored) {
}
}
realm.close();
} catch (
Exception ignored) {
}
this.indexRaw = null;
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);
if (realm2.isInTransaction()) {
realm2.cancelTransaction();
}
// set lastUpdate
realm2.executeTransaction(r -> {
ReposList repoListCache = r.where(ReposList.class).equalTo("id", this.repoData.id).findFirst();
if (repoListCache != null) {
success.set(true);
// get unix timestamp of current time
int currentTime = (int) (System.currentTimeMillis() / 1000);
Timber.d("Updating lastUpdate for repo %s to %s which is %s seconds ago", this.repoData.id, currentTime, (currentTime - repoListCache.getLastUpdate()));
repoListCache.setLastUpdate(currentTime);
} else {
Timber.w("Failed to update lastUpdate for repo %s", this.repoData.id);
}
});
realm2.close();
} else {
success.set(true); // assume we're reading from cache. this may be unsafe but it's better than nothing
}
return success.get();
}
}