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 toUpdate; private Collection 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 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 toUpdate() { return this.toUpdate; } public Collection 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= //name= //version= //versionCode= //author= //description= //minApi= //maxApi= //minMagisk= //needRamdisk= //support= //donate= //config= //changeBoot= //mmtReborn= // extra properties only useful for the database //repoId= //installed= //installedVersionCode= (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(); } }