diff --git a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java index 0fd2e568a..ff9cafa66 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -184,6 +184,11 @@ public class AppProvider extends FDroidProvider { public static List findCanUpdate(Context context, String[] projection) { return cursorToList(context.getContentResolver().query(AppProvider.getCanUpdateUri(), projection, null, null, null)); } + + public static void recalculatePreferredMetadata(Context context) { + Uri uri = Uri.withAppendedPath(AppProvider.getContentUri(), PATH_CALC_PREFERRED_METADATA); + context.getContentResolver().query(uri, null, null, null, null); + } } /** @@ -445,6 +450,7 @@ public class AppProvider extends FDroidProvider { private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex"; private static final String PATH_REPO = "repo"; private static final String PATH_HIGHEST_PRIORITY = "highestPriority"; + private static final String PATH_CALC_PREFERRED_METADATA = "calcPreferredMetadata"; private static final int CAN_UPDATE = CODE_SINGLE + 1; private static final int INSTALLED = CAN_UPDATE + 1; @@ -459,6 +465,7 @@ public class AppProvider extends FDroidProvider { private static final int SEARCH_INSTALLED = SEARCH_REPO + 1; private static final int SEARCH_CAN_UPDATE = SEARCH_INSTALLED + 1; private static final int HIGHEST_PRIORITY = SEARCH_CAN_UPDATE + 1; + private static final int CALC_PREFERRED_METADATA = HIGHEST_PRIORITY + 1; static { MATCHER.addURI(getAuthority(), null, CODE_LIST); @@ -476,6 +483,7 @@ public class AppProvider extends FDroidProvider { MATCHER.addURI(getAuthority(), PATH_NO_APKS, NO_APKS); MATCHER.addURI(getAuthority(), PATH_HIGHEST_PRIORITY + "/*", HIGHEST_PRIORITY); MATCHER.addURI(getAuthority(), PATH_SPECIFIC_APP + "/#/*", CODE_SINGLE); + MATCHER.addURI(getAuthority(), PATH_CALC_PREFERRED_METADATA, CALC_PREFERRED_METADATA); } public static Uri getContentUri() { @@ -719,16 +727,11 @@ public class AppProvider extends FDroidProvider { /** * Ensures that for each app metadata row with the same package name, only the one from the repo - * with the best priority is represented in the result set. + * with the best priority is represented in the result set. While possible to calculate this + * dynamically each time the query is run, we precalculate it during repo updates for performance. */ private AppQuerySelection queryHighestPriority() { - final String highestPriority = - "SELECT MIN(r." + RepoTable.Cols.PRIORITY + ") " + - "FROM " + RepoTable.NAME + " AS r " + - "JOIN " + AppMetadataTable.NAME + " AS m ON (m." + Cols.REPO_ID + " = r." + RepoTable.Cols._ID + ") " + - "WHERE m." + Cols.PACKAGE_ID + " = " + getTableName() + "." + Cols.PACKAGE_ID; - - final String selection = RepoTable.NAME + "." + RepoTable.Cols.PRIORITY + " = (" + highestPriority + ")"; + final String selection = PackageTable.NAME + "." + PackageTable.Cols.PREFERRED_METADATA + " = " + getTableName() + "." + Cols.ROW_ID; return new AppQuerySelection(selection); } @@ -793,6 +796,10 @@ public class AppProvider extends FDroidProvider { boolean repoIsKnown = false; switch (MATCHER.match(uri)) { + case CALC_PREFERRED_METADATA: + updatePreferredMetadata(); + return null; + case CODE_LIST: includeSwap = false; break; @@ -937,12 +944,37 @@ public class AppProvider extends FDroidProvider { } protected void updateAppDetails() { + updatePreferredMetadata(); updateCompatibleFlags(); updateSuggestedFromUpstream(); updateSuggestedFromLatest(); updateIconUrls(getContext(), db(), getTableName(), getApkTableName()); } + private void updatePreferredMetadata() { + Utils.debugLog(TAG, "Deciding on which metadata should take priority for each package."); + + final String app = getTableName(); + + final String highestPriority = + "SELECT MIN(r." + RepoTable.Cols.PRIORITY + ") " + + "FROM " + RepoTable.NAME + " AS r " + + "JOIN " + getTableName() + " AS m ON (m." + Cols.REPO_ID + " = r." + RepoTable.Cols._ID + ") " + + "WHERE m." + Cols.PACKAGE_ID + " = " + "metadata." + Cols.PACKAGE_ID; + + String updateSql = + "UPDATE " + PackageTable.NAME + " " + + "SET " + PackageTable.Cols.PREFERRED_METADATA + " = ( " + + " SELECT metadata." + Cols.ROW_ID + + " FROM " + app + " AS metadata " + + " JOIN " + RepoTable.NAME + " AS repo ON (metadata." + Cols.REPO_ID + " = repo." + RepoTable.Cols._ID + ") " + + " WHERE metadata." + Cols.PACKAGE_ID + " = " + PackageTable.NAME + "." + PackageTable.Cols.ROW_ID + + " AND repo." + RepoTable.Cols.PRIORITY + " = (" + highestPriority + ")" + + ");"; + + db().execSQL(updateSql); + } + /** * For each app, we want to set the isCompatible flag to 1 if any of the apks we know * about are compatible, and 0 otherwise. diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index d79335940..9c238295d 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -53,7 +53,8 @@ class DBHelper extends SQLiteOpenHelper { private static final String CREATE_TABLE_PACKAGE = "CREATE TABLE " + PackageTable.NAME + " ( " - + PackageTable.Cols.PACKAGE_NAME + " text not null" + + PackageTable.Cols.PACKAGE_NAME + " text not null, " + + PackageTable.Cols.PREFERRED_METADATA + " integer" + ");"; private static final String CREATE_TABLE_REPO = "create table " @@ -838,6 +839,7 @@ class DBHelper extends SQLiteOpenHelper { if (tableExists(db, PackageTable.NAME)) { Utils.debugLog(TAG, "Ensuring indexes exist for " + PackageTable.NAME); db.execSQL("CREATE INDEX IF NOT EXISTS package_packageName on " + PackageTable.NAME + " (" + PackageTable.Cols.PACKAGE_NAME + ");"); + db.execSQL("CREATE INDEX IF NOT EXISTS package_preferredMetadata on " + PackageTable.NAME + " (" + PackageTable.Cols.PREFERRED_METADATA + ");"); } Utils.debugLog(TAG, "Ensuring indexes exist for " + AppMetadataTable.NAME); diff --git a/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java b/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java index da0240a99..e5a6b5347 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java @@ -232,6 +232,8 @@ public class RepoProvider extends FDroidProvider { Uri appUri = AppProvider.getNoApksUri(); int appCount = resolver.delete(appUri, null, null); Utils.debugLog(TAG, "Removed " + appCount + " apps with no apks."); + + AppProvider.Helper.recalculatePreferredMetadata(context); } public static int countAppsForRepo(Context context, long repoId) { @@ -399,7 +401,37 @@ public class RepoProvider extends FDroidProvider { @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { + + // When the priority of a repo changes, we need to update the "preferred metadata" foreign + // key in the package table to point to the best possible record in the app metadata table. + // The full list of times when we need to recalculate the preferred metadata includes: + // * After the priority of a repo changes + // * After a repo is disabled + // * After a repo is enabled + // * After an update is performed + // This code only checks for the priority changing. All other occasions we can't do the + // recalculation right now, because we likely haven't added/removed the relevant apps + // from the metadata table yet. Usually the repo details are updated, then a request is + // made to do the heavier work (e.g. a repo update to get new list of apps from server). + // After the heavier work is complete, then that process can request the preferred metadata + // to be recalculated. + boolean priorityChanged = false; + if (values.containsKey(Cols.PRIORITY)) { + Cursor priorityCursor = db().query(getTableName(), new String[]{Cols.PRIORITY}, where, whereArgs, null, null, null); + if (priorityCursor.getCount() > 0) { + priorityCursor.moveToFirst(); + int oldPriority = priorityCursor.getInt(priorityCursor.getColumnIndex(Cols.PRIORITY)); + priorityChanged = oldPriority != values.getAsInteger(Cols.PRIORITY); + } + priorityCursor.close(); + } + int numRows = db().update(getTableName(), values, where, whereArgs); + + if (priorityChanged) { + AppProvider.Helper.recalculatePreferredMetadata(getContext()); + } + Utils.debugLog(TAG, "Updated repo. Notifying provider change: '" + uri + "'."); getContext().getContentResolver().notifyChange(uri, null); return numRows; diff --git a/app/src/main/java/org/fdroid/fdroid/data/Schema.java b/app/src/main/java/org/fdroid/fdroid/data/Schema.java index 5b1f1779f..edaa7ae14 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Schema.java @@ -17,8 +17,18 @@ public interface Schema { String ROW_ID = "rowid"; String PACKAGE_NAME = "packageName"; + /** + * Metadata about a package (e.g. description, icon, etc) can come from multiple + * different repos. This is a foreign key to the row in {@link AppMetadataTable} for + * this package that comes from the repo with the best priority. Although it can be + * calculated at runtime using an SQL query, it is more efficient to figure out the + * preferred metadata once, after a repo update, rather than every time we need to know + * about a package. + */ + String PREFERRED_METADATA = "preferredMetadata"; + String[] ALL = { - ROW_ID, PACKAGE_NAME, + ROW_ID, PACKAGE_NAME, PREFERRED_METADATA, }; } } @@ -101,7 +111,7 @@ public interface Schema { * or which are from other related tables (e.g. {@link Cols.SuggestedApk#VERSION_NAME}). */ String[] ALL_COLS = { - ROW_ID, PACKAGE_ID, IS_COMPATIBLE, NAME, SUMMARY, ICON, DESCRIPTION, + ROW_ID, PACKAGE_ID, REPO_ID, IS_COMPATIBLE, NAME, SUMMARY, ICON, DESCRIPTION, LICENSE, AUTHOR, EMAIL, WEB_URL, TRACKER_URL, SOURCE_URL, CHANGELOG_URL, DONATE_URL, BITCOIN_ADDR, LITECOIN_ADDR, FLATTR_ID, UPSTREAM_VERSION_NAME, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED, diff --git a/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java b/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java index 73b8cd1da..0d8102b8d 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java @@ -371,6 +371,9 @@ public class AppProviderTest extends FDroidProviderTest { Uri uri = AppProvider.getContentUri(); contentResolver.insert(uri, values); + + AppProvider.Helper.recalculatePreferredMetadata(context); + return AppProvider.Helper.findSpecificApp(context.getContentResolver(), id, 1, Cols.ALL); } }