package de.robv.android.xposed.installer.repo; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Build; import android.text.TextUtils; import android.util.Pair; import java.io.File; import java.util.LinkedHashMap; import java.util.Map; import de.robv.android.xposed.installer.XposedApp; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.InstalledModulesColumns; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.InstalledModulesUpdatesColumns; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.ModuleVersionsColumns; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.ModulesColumns; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.MoreInfoColumns; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.OverviewColumns; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.OverviewColumnsIndexes; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.RepositoriesColumns; import de.robv.android.xposed.installer.util.ModuleUtil; import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; import de.robv.android.xposed.installer.util.RepoLoader; public final class RepoDb extends SQLiteOpenHelper { public static final int SORT_STATUS = 0; public static final int SORT_UPDATED = 1; public static final int SORT_CREATED = 2; private static SQLiteDatabase sDb; private RepoDb(Context context) { super(context, getDbPath(context), null, RepoDbDefinitions.DATABASE_VERSION); } private static String getDbPath(Context context) { if (Build.VERSION.SDK_INT >= 21) { return new File(context.getNoBackupFilesDir(), RepoDbDefinitions.DATABASE_NAME).getPath(); } else { return RepoDbDefinitions.DATABASE_NAME; } } static { RepoDb instance = new RepoDb(XposedApp.getInstance()); sDb = instance.getWritableDatabase(); sDb.execSQL("PRAGMA foreign_keys=ON"); instance.createTempTables(sDb); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_REPOSITORIES); db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULES); db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULE_VERSIONS); db.execSQL(RepoDbDefinitions.SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID); db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MORE_INFO); RepoLoader.getInstance().clear(false); } private void createTempTables(SQLiteDatabase db) { db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES); db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // This is only a cache, so simply drop & recreate the tables db.execSQL("DROP TABLE IF EXISTS " + RepositoriesColumns.TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + ModulesColumns.TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + ModuleVersionsColumns.TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + MoreInfoColumns.TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + InstalledModulesColumns.TABLE_NAME); db.execSQL("DROP VIEW IF EXISTS " + InstalledModulesUpdatesColumns.VIEW_NAME); onCreate(db); } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { onUpgrade(db, oldVersion, newVersion); } public static void beginTransation() { sDb.beginTransaction(); } public static void setTransactionSuccessful() { sDb.setTransactionSuccessful(); } public static void endTransation() { sDb.endTransaction(); } private static String getString(String table, String searchColumn, String searchValue, String resultColumn) { String[] projection = new String[]{resultColumn}; String where = searchColumn + " = ?"; String[] whereArgs = new String[]{searchValue}; Cursor c = sDb.query(table, projection, where, whereArgs, null, null, null, "1"); if (c.moveToFirst()) { String result = c.getString(c.getColumnIndexOrThrow(resultColumn)); c.close(); return result; } else { c.close(); throw new RowNotFoundException("Could not find " + table + "." + searchColumn + " with value '" + searchValue + "'"); } } public static long insertRepository(String url) { ContentValues values = new ContentValues(); values.put(RepositoriesColumns.URL, url); return sDb.insertOrThrow(RepositoriesColumns.TABLE_NAME, null, values); } public static void deleteRepositories() { if (sDb != null) sDb.delete(RepositoriesColumns.TABLE_NAME, null, null); } public static Map<Long, Repository> getRepositories() { Map<Long, Repository> result = new LinkedHashMap<Long, Repository>(1); String[] projection = new String[]{ RepositoriesColumns._ID, RepositoriesColumns.URL, RepositoriesColumns.TITLE, RepositoriesColumns.PARTIAL_URL, RepositoriesColumns.VERSION, }; Cursor c = sDb.query(RepositoriesColumns.TABLE_NAME, projection, null, null, null, null, RepositoriesColumns._ID); while (c.moveToNext()) { Repository repo = new Repository(); long id = c.getLong(c.getColumnIndexOrThrow(RepositoriesColumns._ID)); repo.url = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.URL)); repo.name = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.TITLE)); repo.partialUrl = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.PARTIAL_URL)); repo.version = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.VERSION)); result.put(id, repo); } c.close(); return result; } public static void updateRepository(long repoId, Repository repository) { ContentValues values = new ContentValues(); values.put(RepositoriesColumns.TITLE, repository.name); values.put(RepositoriesColumns.PARTIAL_URL, repository.partialUrl); values.put(RepositoriesColumns.VERSION, repository.version); sDb.update(RepositoriesColumns.TABLE_NAME, values, RepositoriesColumns._ID + " = ?", new String[]{Long.toString(repoId)}); } public static void updateRepositoryVersion(long repoId, String version) { ContentValues values = new ContentValues(); values.put(RepositoriesColumns.VERSION, version); sDb.update(RepositoriesColumns.TABLE_NAME, values, RepositoriesColumns._ID + " = ?", new String[]{Long.toString(repoId)}); } public static long insertModule(long repoId, Module mod) { ContentValues values = new ContentValues(); values.put(ModulesColumns.REPO_ID, repoId); values.put(ModulesColumns.PKGNAME, mod.packageName); values.put(ModulesColumns.TITLE, mod.name); values.put(ModulesColumns.SUMMARY, mod.summary); values.put(ModulesColumns.DESCRIPTION, mod.description); values.put(ModulesColumns.DESCRIPTION_IS_HTML, mod.descriptionIsHtml); values.put(ModulesColumns.AUTHOR, mod.author); values.put(ModulesColumns.SUPPORT, mod.support); values.put(ModulesColumns.CREATED, mod.created); values.put(ModulesColumns.UPDATED, mod.updated); ModuleVersion latestVersion = RepoLoader.getInstance().getLatestVersion(mod); sDb.beginTransaction(); try { long moduleId = sDb.insertOrThrow(ModulesColumns.TABLE_NAME, null, values); long latestVersionId = -1; for (ModuleVersion version : mod.versions) { long versionId = insertModuleVersion(moduleId, version); if (latestVersion == version) latestVersionId = versionId; } if (latestVersionId > -1) { values = new ContentValues(); values.put(ModulesColumns.LATEST_VERSION, latestVersionId); sDb.update(ModulesColumns.TABLE_NAME, values, ModulesColumns._ID + " = ?", new String[]{Long.toString(moduleId)}); } for (Pair<String, String> moreInfoEntry : mod.moreInfo) { insertMoreInfo(moduleId, moreInfoEntry.first, moreInfoEntry.second); } // TODO Add mod.screenshots sDb.setTransactionSuccessful(); return moduleId; } finally { sDb.endTransaction(); } } private static long insertModuleVersion(long moduleId, ModuleVersion version) { ContentValues values = new ContentValues(); values.put(ModuleVersionsColumns.MODULE_ID, moduleId); values.put(ModuleVersionsColumns.NAME, version.name); values.put(ModuleVersionsColumns.CODE, version.code); values.put(ModuleVersionsColumns.DOWNLOAD_LINK, version.downloadLink); values.put(ModuleVersionsColumns.MD5SUM, version.md5sum); values.put(ModuleVersionsColumns.CHANGELOG, version.changelog); values.put(ModuleVersionsColumns.CHANGELOG_IS_HTML, version.changelogIsHtml); values.put(ModuleVersionsColumns.RELTYPE, version.relType.ordinal()); values.put(ModuleVersionsColumns.UPLOADED, version.uploaded); return sDb.insertOrThrow(ModuleVersionsColumns.TABLE_NAME, null, values); } private static long insertMoreInfo(long moduleId, String title, String value) { ContentValues values = new ContentValues(); values.put(MoreInfoColumns.MODULE_ID, moduleId); values.put(MoreInfoColumns.LABEL, title); values.put(MoreInfoColumns.VALUE, value); return sDb.insertOrThrow(MoreInfoColumns.TABLE_NAME, null, values); } public static void deleteAllModules(long repoId) { sDb.delete(ModulesColumns.TABLE_NAME, ModulesColumns.REPO_ID + " = ?", new String[]{Long.toString(repoId)}); } public static void deleteModule(long repoId, String packageName) { sDb.delete(ModulesColumns.TABLE_NAME, ModulesColumns.REPO_ID + " = ? AND " + ModulesColumns.PKGNAME + " = ?", new String[]{Long.toString(repoId), packageName}); } public static Module getModuleByPackageName(String packageName) { // The module itself String[] projection = new String[]{ ModulesColumns._ID, ModulesColumns.REPO_ID, ModulesColumns.PKGNAME, ModulesColumns.TITLE, ModulesColumns.SUMMARY, ModulesColumns.DESCRIPTION, ModulesColumns.DESCRIPTION_IS_HTML, ModulesColumns.AUTHOR, ModulesColumns.SUPPORT, ModulesColumns.CREATED, ModulesColumns.UPDATED, }; String where = ModulesColumns.PREFERRED + " = 1 AND " + ModulesColumns.PKGNAME + " = ?"; String[] whereArgs = new String[]{packageName}; Cursor c = sDb.query(ModulesColumns.TABLE_NAME, projection, where, whereArgs, null, null, null, "1"); if (!c.moveToFirst()) { c.close(); return null; } long moduleId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns._ID)); long repoId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.REPO_ID)); Module mod = new Module(RepoLoader.getInstance().getRepository(repoId)); mod.packageName = c.getString(c.getColumnIndexOrThrow(ModulesColumns.PKGNAME)); mod.name = c.getString(c.getColumnIndexOrThrow(ModulesColumns.TITLE)); mod.summary = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUMMARY)); mod.description = c.getString(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION)); mod.descriptionIsHtml = c.getInt(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION_IS_HTML)) > 0; mod.author = c.getString(c.getColumnIndexOrThrow(ModulesColumns.AUTHOR)); mod.support = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUPPORT)); mod.created = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.CREATED)); mod.updated = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.UPDATED)); c.close(); // Versions projection = new String[]{ ModuleVersionsColumns.NAME, ModuleVersionsColumns.CODE, ModuleVersionsColumns.DOWNLOAD_LINK, ModuleVersionsColumns.MD5SUM, ModuleVersionsColumns.CHANGELOG, ModuleVersionsColumns.CHANGELOG_IS_HTML, ModuleVersionsColumns.RELTYPE, ModuleVersionsColumns.UPLOADED, }; where = ModuleVersionsColumns.MODULE_ID + " = ?"; whereArgs = new String[]{Long.toString(moduleId)}; c = sDb.query(ModuleVersionsColumns.TABLE_NAME, projection, where, whereArgs, null, null, null); while (c.moveToNext()) { ModuleVersion version = new ModuleVersion(mod); version.name = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.NAME)); version.code = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CODE)); version.downloadLink = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.DOWNLOAD_LINK)); version.md5sum = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.MD5SUM)); version.changelog = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG)); version.changelogIsHtml = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG_IS_HTML)) > 0; version.relType = ReleaseType.fromOrdinal(c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.RELTYPE))); version.uploaded = c.getLong(c.getColumnIndexOrThrow(ModuleVersionsColumns.UPLOADED)); mod.versions.add(version); } c.close(); // MoreInfo projection = new String[]{ MoreInfoColumns.LABEL, MoreInfoColumns.VALUE, }; where = MoreInfoColumns.MODULE_ID + " = ?"; whereArgs = new String[]{Long.toString(moduleId)}; c = sDb.query(MoreInfoColumns.TABLE_NAME, projection, where, whereArgs, null, null, MoreInfoColumns._ID); while (c.moveToNext()) { String label = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.LABEL)); String value = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.VALUE)); mod.moreInfo.add(new Pair<>(label, value)); } c.close(); return mod; } public static String getModuleSupport(String packageName) { return getString(ModulesColumns.TABLE_NAME, ModulesColumns.PKGNAME, packageName, ModulesColumns.SUPPORT); } public static void updateModuleLatestVersion(String packageName) { int maxShownReleaseType = RepoLoader.getInstance().getMaxShownReleaseType(packageName).ordinal(); sDb.execSQL("UPDATE " + ModulesColumns.TABLE_NAME + " SET " + ModulesColumns.LATEST_VERSION + " = (SELECT " + ModuleVersionsColumns._ID + " FROM " + ModuleVersionsColumns.TABLE_NAME + " AS v" + " WHERE v." + ModuleVersionsColumns.MODULE_ID + " = " + ModulesColumns.TABLE_NAME + "." + ModulesColumns._ID + " AND reltype <= ? LIMIT 1)" + " WHERE " + ModulesColumns.PKGNAME + " = ?", new Object[]{maxShownReleaseType, packageName}); } public static void updateAllModulesLatestVersion() { sDb.beginTransaction(); try { String[] projection = new String[]{ModulesColumns.PKGNAME}; Cursor c = sDb.query(true, ModulesColumns.TABLE_NAME, projection, null, null, null, null, null, null); while (c.moveToNext()) { updateModuleLatestVersion(c.getString(0)); } c.close(); sDb.setTransactionSuccessful(); } finally { sDb.endTransaction(); } } public static long insertInstalledModule(InstalledModule installed) { ContentValues values = new ContentValues(); values.put(InstalledModulesColumns.PKGNAME, installed.packageName); values.put(InstalledModulesColumns.VERSION_CODE, installed.versionCode); values.put(InstalledModulesColumns.VERSION_NAME, installed.versionName); return sDb.insertOrThrow(InstalledModulesColumns.TABLE_NAME, null, values); } public static void deleteInstalledModule(String packageName) { sDb.delete(InstalledModulesColumns.TABLE_NAME, InstalledModulesColumns.PKGNAME + " = ?", new String[]{packageName}); } public static void deleteAllInstalledModules() { sDb.delete(InstalledModulesColumns.TABLE_NAME, null, null); } public static Cursor queryModuleOverview(int sortingOrder, CharSequence filterText) { // Columns String[] projection = new String[]{ "m." + ModulesColumns._ID, "m." + ModulesColumns.PKGNAME, "m." + ModulesColumns.TITLE, "m." + ModulesColumns.SUMMARY, "m." + ModulesColumns.CREATED, "m." + ModulesColumns.UPDATED, "v." + ModuleVersionsColumns.NAME + " AS " + OverviewColumns.LATEST_VERSION, "i." + InstalledModulesColumns.VERSION_NAME + " AS " + OverviewColumns.INSTALLED_VERSION, "(CASE WHEN m." + ModulesColumns.PKGNAME + " = '" + ModuleUtil.getInstance().getFrameworkPackageName() + "' THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_FRAMEWORK, "(CASE WHEN i." + InstalledModulesColumns.VERSION_NAME + " IS NOT NULL" + " THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_INSTALLED, "(CASE WHEN v." + ModuleVersionsColumns.CODE + " > " + InstalledModulesColumns.VERSION_CODE + " THEN 1 ELSE 0 END) AS " + OverviewColumns.HAS_UPDATE, }; // Conditions String where = ModulesColumns.PREFERRED + " = 1"; String whereArgs[] = null; if (!TextUtils.isEmpty(filterText)) { where += " AND (m." + ModulesColumns.TITLE + " LIKE ?" + " OR m." + ModulesColumns.SUMMARY + " LIKE ?" + " OR m." + ModulesColumns.DESCRIPTION + " LIKE ?" + " OR m." + ModulesColumns.AUTHOR + " LIKE ?)"; String filterTextArg = "%" + filterText + "%"; whereArgs = new String[]{filterTextArg, filterTextArg, filterTextArg, filterTextArg}; } // Sorting order StringBuilder sbOrder = new StringBuilder(); if (sortingOrder == SORT_CREATED) { sbOrder.append(OverviewColumns.CREATED); sbOrder.append(" DESC,"); } else if (sortingOrder == SORT_UPDATED) { sbOrder.append(OverviewColumns.UPDATED); sbOrder.append(" DESC,"); } sbOrder.append(OverviewColumns.IS_FRAMEWORK); sbOrder.append(" DESC, "); sbOrder.append(OverviewColumns.HAS_UPDATE); sbOrder.append(" DESC, "); sbOrder.append(OverviewColumns.IS_INSTALLED); sbOrder.append(" DESC, "); sbOrder.append("m."); sbOrder.append(OverviewColumns.TITLE); sbOrder.append(" COLLATE NOCASE, "); sbOrder.append("m."); sbOrder.append(OverviewColumns.PKGNAME); // Query Cursor c = sDb.query( ModulesColumns.TABLE_NAME + " AS m" + " LEFT JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v" + " ON v." + ModuleVersionsColumns._ID + " = m." + ModulesColumns.LATEST_VERSION + " LEFT JOIN " + InstalledModulesColumns.TABLE_NAME + " AS i" + " ON i." + InstalledModulesColumns.PKGNAME + " = m." + ModulesColumns.PKGNAME, projection, where, whereArgs, null, null, sbOrder.toString()); // Cache column indexes OverviewColumnsIndexes.fillFromCursor(c); return c; } public static String getFrameworkUpdateVersion() { return getFirstUpdate(true); } public static boolean hasModuleUpdates() { return getFirstUpdate(false) != null; } private static String getFirstUpdate(boolean framework) { String[] projection = new String[]{InstalledModulesUpdatesColumns.LATEST_NAME}; String where = ModulesColumns.PKGNAME + (framework ? " = ?" : " != ?"); String[] whereArgs = new String[]{ModuleUtil.getInstance().getFrameworkPackageName()}; Cursor c = sDb.query(InstalledModulesUpdatesColumns.VIEW_NAME, projection, where, whereArgs, null, null, null, "1"); String latestVersion = null; if (c.moveToFirst()) latestVersion = c.getString(c.getColumnIndexOrThrow(InstalledModulesUpdatesColumns.LATEST_NAME)); c.close(); return latestVersion; } public static class RowNotFoundException extends RuntimeException { private static final long serialVersionUID = -396324186622439535L; public RowNotFoundException(String reason) { super(reason); } } }