/* * 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.room.Room 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.room.ModuleListCacheDatabase import com.fox2code.mmm.utils.room.ReposListDatabase import org.json.JSONException import org.json.JSONObject import timber.log.Timber import java.io.File import java.io.IOException @Suppress("LeakingThis", "SENSELESS_COMPARISON", "RedundantSetter") open class RepoData(url: String, cacheRoot: File) : XRepo() { private val supportedProperties = JSONObject() private val populateLock = Any() open var url: String = "" var preferenceId: String? = null var cacheRoot: File var moduleHashMap: HashMap private var metaDataCache: JSONObject? var lastUpdate: Long = 0 var website: String? = null var support: String? = null var donate: String? = null var submitModule: String? = null var defaultName: String var defaultWebsite: String protected var defaultSupport: String? = null protected var defaultDonate: String? = null var defaultSubmitModule: String? = null override var name: String? = null get() = field ?: defaultName set(value) { field = value } // 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) 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) // basically same as above but for room database val db = Room.databaseBuilder( INSTANCE!!.applicationContext, ReposListDatabase::class.java, "ReposList.db" ).allowMainThreadQueries().build() val reposListRoom = db.reposListDao() val reposListRoomList = reposListRoom.getById(preferenceId!!) enabled = !isForceHide && reposListRoomList != null && reposListRoomList.enabled defaultWebsite = "https://" + Uri.parse(url).host + "/" // open realm database // load metadata from realm database if (enabled) { try { // 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 the database name = reposListRoomList.name website = reposListRoomList.website support = reposListRoomList.support donate = reposListRoomList.donate submitModule = reposListRoomList.submitModule // if name is null return defaultName and if defaultName is null return url if (name == null) { name = if (defaultName == null) { url } else { defaultName } } } } catch (e: Exception) { Timber.w("Failed to load repo metadata from database: " + e.message + ". If this is a first time run, this is normal.") } } db.close() } open fun prepare(): Boolean { return true } @Throws(JSONException::class) open fun populate(jsonObject: JSONObject): List? { val newModules: MutableList = 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 db = Room.databaseBuilder( INSTANCE!!.applicationContext, ReposListDatabase::class.java, "ReposList.db", ).allowMainThreadQueries().build() val reposList = db.reposListDao().getById(preferenceId!!) db.close() // should never happen but for safety if (reposList.enabled) { !isForceHide } else { false } } set(value) { field = value this.enabled = enabled && !isForceHide val db = Room.databaseBuilder( INSTANCE!!.applicationContext, ReposListDatabase::class.java, "ReposList.db", ).allowMainThreadQueries().build() val reposList = db.reposListDao().getById(preferenceId!!) db.reposListDao().update( name = reposList.name, enabled = value, id = reposList.id, donate = reposList.donate.toString(), support = reposList.support.toString(), website = reposList.website.toString(), submitModule = reposList.submitModule.toString(), lastUpdate = reposList.lastUpdate.toLong(), url = reposList.url ) db.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 { if (BuildConfig.DEBUG) 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!!) val db = Room.databaseBuilder( INSTANCE!!.applicationContext, ReposListDatabase::class.java, "ReposList.db", ).allowMainThreadQueries().build() val reposList = db.reposListDao().getById(preferenceId!!) enabled = if (reposList.enabled) { !isForceHide } else { false } } // should update (lastUpdate > 15 minutes) fun shouldUpdate(): Boolean { if (BuildConfig.DEBUG) Timber.d("Repo $preferenceId should update check called") val db = Room.databaseBuilder( INSTANCE!!.applicationContext, ReposListDatabase::class.java, "ReposList.db", ).allowMainThreadQueries().build() val repo = db.reposListDao().getById(preferenceId!!) // get modulelistcache val db2 = Room.databaseBuilder( INSTANCE!!.applicationContext, ModuleListCacheDatabase::class.java, "ModuleListCache.db", ).allowMainThreadQueries().build() val moduleListCache = db2.moduleListCacheDao().getByRepoId(preferenceId!!) if (repo != null) { return if (repo.lastUpdate != 0 && moduleListCache.isNotEmpty() && moduleListCache.size > 1) { val lastUpdate = repo.lastUpdate.toLong() val currentTime = System.currentTimeMillis() val diff = currentTime - lastUpdate val diffMinutes = diff / (60 * 1000) % 60 if (BuildConfig.DEBUG) Timber.d("Repo $preferenceId updated: $diffMinutes minutes ago") db.close() db2.close() diffMinutes > if (BuildConfig.DEBUG) 15 else 30 } else { if (BuildConfig.DEBUG) Timber.d("Repo $preferenceId shouldUpdate true: lastUpdate is " + repo.lastUpdate + " and moduleListCache is " + moduleListCache.size) db.close() db2.close() true } } else { db.close() db2.close() } return true } companion object }