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 7913bddbc..c35669d07 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -117,6 +117,11 @@ public class AppProvider extends FDroidProvider { return app; } + public static void calcSuggestedApk(Context context, String packageName) { + Uri uri = Uri.withAppendedPath(calcSuggestedApksUri(), packageName); + context.getContentResolver().update(uri, null, null, null); + } + public static void calcSuggestedApks(Context context) { context.getContentResolver().update(calcSuggestedApksUri(), null, null, null); } @@ -385,6 +390,7 @@ public class AppProvider extends FDroidProvider { static { MATCHER.addURI(getAuthority(), null, CODE_LIST); MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS, CALC_SUGGESTED_APKS); + MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS + "/*", CALC_SUGGESTED_APKS); MATCHER.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED); MATCHER.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY); MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*/*", SEARCH_TEXT_AND_CATEGORIES); @@ -879,7 +885,13 @@ public class AppProvider extends FDroidProvider { throw new UnsupportedOperationException("Update not supported for " + uri + "."); } - updateSuggestedApks(); + List segments = uri.getPathSegments(); + if (segments.size() > 1) { + String packageName = segments.get(1); + updateSuggestedApk(packageName); + } else { + updateSuggestedApks(); + } getContext().getContentResolver().notifyChange(getCanUpdateUri(), null); return 0; } @@ -887,8 +899,8 @@ public class AppProvider extends FDroidProvider { protected void updateAllAppDetails() { updatePreferredMetadata(); updateCompatibleFlags(); - updateSuggestedFromUpstream(); - updateSuggestedFromLatest(); + updateSuggestedFromUpstream(null); + updateSuggestedFromLatest(null); updateIconUrls(); } @@ -909,8 +921,13 @@ public class AppProvider extends FDroidProvider { * {@link android.app.IntentService} as described in https://gitlab.com/fdroid/fdroidclient/issues/520. */ protected void updateSuggestedApks() { - updateSuggestedFromUpstream(); - updateSuggestedFromLatest(); + updateSuggestedFromUpstream(null); + updateSuggestedFromLatest(null); + } + + protected void updateSuggestedApk(String packageName) { + updateSuggestedFromUpstream(packageName); + updateSuggestedFromLatest(packageName); } private void updatePreferredMetadata() { @@ -964,9 +981,9 @@ public class AppProvider extends FDroidProvider { * If the app is installed, then all apks signed by a different certificate are * ignored for the purpose of this calculation. * - * @see #updateSuggestedFromLatest() + * @see #updateSuggestedFromLatest(String) */ - private void updateSuggestedFromUpstream() { + private void updateSuggestedFromUpstream(@Nullable String packageName) { Utils.debugLog(TAG, "Calculating suggested versions for all NON-INSTALLED apps which specify an upstream version code."); final String apk = getApkTableName(); @@ -976,6 +993,14 @@ public class AppProvider extends FDroidProvider { final boolean unstableUpdates = Preferences.get().getUnstableUpdates(); String restrictToStable = unstableUpdates ? "" : (apk + "." + ApkTable.Cols.VERSION_CODE + " <= " + app + "." + Cols.UPSTREAM_VERSION_CODE + " AND "); + String restrictToApp = ""; + String[] args = null; + + if (packageName != null) { + restrictToApp = " AND " + app + "." + Cols.PACKAGE_ID + " = (" + getPackageIdFromPackageNameQuery() + ") "; + args = new String[]{packageName}; + } + // The join onto `appForThisApk` is to ensure that the MAX(apk.versionCode) is chosen from // all apps regardless of repo. If we joined directly onto the outer `app` table we are // in the process of updating, then it would be limited to only apks from the same repo. @@ -1001,9 +1026,9 @@ public class AppProvider extends FDroidProvider { apk + "." + ApkTable.Cols.SIGNATURE + " = COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " + restrictToStable + " ( " + app + "." + Cols.IS_COMPATIBLE + " = 0 OR " + apk + "." + Cols.IS_COMPATIBLE + " = 1 ) ) " + - " WHERE " + Cols.UPSTREAM_VERSION_CODE + " > 0 "; + " WHERE " + Cols.UPSTREAM_VERSION_CODE + " > 0 " + restrictToApp; - LoggingQuery.execSQL(db(), updateSql); + LoggingQuery.execSQL(db(), updateSql, args); } /** @@ -1014,15 +1039,28 @@ public class AppProvider extends FDroidProvider { * out from the upstream vercode. In such a case, fall back to the simpler * algorithm as if upstreamVercode was 0. * - * @see #updateSuggestedFromUpstream() + * @see #updateSuggestedFromUpstream(String) */ - private void updateSuggestedFromLatest() { + private void updateSuggestedFromLatest(@Nullable String packageName) { Utils.debugLog(TAG, "Calculating suggested versions for all apps which don't specify an upstream version code."); final String apk = getApkTableName(); final String app = getTableName(); final String installed = InstalledAppTable.NAME; + final String restrictToApps; + final String[] args; + + if (packageName == null) { + restrictToApps = " COALESCE(" + Cols.UPSTREAM_VERSION_CODE + ", 0) = 0 OR " + Cols.SUGGESTED_VERSION_CODE + " IS NULL "; + args = null; + } else { + // Don't update an app with an upstream version code, because that would have been updated + // by updateSuggestedFromUpdate(packageName). + restrictToApps = " COALESCE(" + Cols.UPSTREAM_VERSION_CODE + ", 0) = 0 AND " + app + "." + Cols.PACKAGE_ID + " = (" + getPackageIdFromPackageNameQuery() + ") "; + args = new String[]{packageName}; + } + String updateSql = "UPDATE " + app + " SET " + Cols.SUGGESTED_VERSION_CODE + " = ( " + " SELECT MAX( " + apk + "." + ApkTable.Cols.VERSION_CODE + " ) " + @@ -1033,9 +1071,9 @@ public class AppProvider extends FDroidProvider { app + "." + Cols.PACKAGE_ID + " = appForThisApk." + Cols.PACKAGE_ID + " AND " + apk + "." + ApkTable.Cols.SIGNATURE + " = COALESCE(" + installed + "." + InstalledAppTable.Cols.SIGNATURE + ", " + apk + "." + ApkTable.Cols.SIGNATURE + ") AND " + " ( " + app + "." + Cols.IS_COMPATIBLE + " = 0 OR " + apk + "." + ApkTable.Cols.IS_COMPATIBLE + " = 1 ) ) " + - " WHERE COALESCE(" + Cols.UPSTREAM_VERSION_CODE + ", 0) = 0 OR " + Cols.SUGGESTED_VERSION_CODE + " IS NULL "; + " WHERE " + restrictToApps; - LoggingQuery.execSQL(db(), updateSql); + LoggingQuery.execSQL(db(), updateSql, args); } private void updateIconUrls() { diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java index ec0396d65..3ecc7fa05 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java @@ -221,10 +221,17 @@ public class InstalledAppProvider extends FDroidProvider { throw new UnsupportedOperationException("Delete not supported for " + uri + "."); } + String packageName = uri.getLastPathSegment(); QuerySelection query = new QuerySelection(where, whereArgs); - query = query.add(queryAppSubQuery(uri.getLastPathSegment())); + query = query.add(queryAppSubQuery(packageName)); - return db().delete(getTableName(), query.getSelection(), query.getArgs()); + Utils.debugLog(TAG, "Deleting " + packageName); + int count = db().delete(getTableName(), query.getSelection(), query.getArgs()); + + Utils.debugLog(TAG, "Requesting the suggested apk get recalculated for " + packageName); + AppProvider.Helper.calcSuggestedApk(getContext(), packageName); + + return count; } @Override @@ -234,15 +241,23 @@ public class InstalledAppProvider extends FDroidProvider { throw new UnsupportedOperationException("Insert not supported for " + uri + "."); } - if (values.containsKey(Cols.Package.NAME)) { - String packageName = values.getAsString(Cols.Package.NAME); - long packageId = PackageProvider.Helper.ensureExists(getContext(), packageName); - values.remove(Cols.Package.NAME); - values.put(Cols.PACKAGE_ID, packageId); + if (!values.containsKey(Cols.Package.NAME)) { + throw new IllegalStateException("Package name not provided to InstalledAppProvider"); } + String packageName = values.getAsString(Cols.Package.NAME); + long packageId = PackageProvider.Helper.ensureExists(getContext(), packageName); + values.remove(Cols.Package.NAME); + values.put(Cols.PACKAGE_ID, packageId); + verifyVersionNameNotNull(values); + + Utils.debugLog(TAG, "Inserting/updating " + packageName); db().replaceOrThrow(getTableName(), null, values); + + Utils.debugLog(TAG, "Requesting the suggested apk get recalculated for " + packageName); + AppProvider.Helper.calcSuggestedApk(getContext(), packageName); + return getAppUri(values.getAsString(Cols.Package.NAME)); } diff --git a/app/src/main/java/org/fdroid/fdroid/data/LoggingQuery.java b/app/src/main/java/org/fdroid/fdroid/data/LoggingQuery.java index db6731307..bf257a0de 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/LoggingQuery.java +++ b/app/src/main/java/org/fdroid/fdroid/data/LoggingQuery.java @@ -69,14 +69,21 @@ final class LoggingQuery { private void execSQLInternal() { if (BuildConfig.DEBUG) { long startTime = System.currentTimeMillis(); - db.execSQL(query); long queryDuration = System.currentTimeMillis() - startTime; - + executeSQLInternal(); if (queryDuration >= SLOW_QUERY_DURATION) { logSlowQuery(queryDuration); } } else { + executeSQLInternal(); + } + } + + private void executeSQLInternal() { + if (queryArgs == null || queryArgs.length == 0) { db.execSQL(query); + } else { + db.execSQL(query, queryArgs); } } @@ -131,7 +138,7 @@ final class LoggingQuery { return new LoggingQuery(db, query, queryBuilderArgs).rawQuery(); } - public static void execSQL(SQLiteDatabase db, String sql) { - new LoggingQuery(db, sql, null).execSQLInternal(); + public static void execSQL(SQLiteDatabase db, String sql, String[] queryArgs) { + new LoggingQuery(db, sql, queryArgs).execSQLInternal(); } } diff --git a/app/src/test/java/org/fdroid/fdroid/TestUtils.java b/app/src/test/java/org/fdroid/fdroid/TestUtils.java index 837f35db7..0b9d6d313 100644 --- a/app/src/test/java/org/fdroid/fdroid/TestUtils.java +++ b/app/src/test/java/org/fdroid/fdroid/TestUtils.java @@ -8,6 +8,7 @@ import android.content.ContextWrapper; import android.content.pm.ProviderInfo; import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.data.RepoProviderTest; @@ -157,4 +158,15 @@ public class TestUtils { } }; } + + /** + * Normally apps/apks are only added to the database in response to a repo update. + * At the end of a repo update, the {@link AppProvider} updates the suggested apks and + * recalculates the preferred metadata for each app. Because we are adding apps/apks + * directly to the database, we need to simulate this update after inserting stuff. + */ + public static void updateDbAfterInserting(Context context) { + AppProvider.Helper.calcSuggestedApks(context); + AppProvider.Helper.recalculatePreferredMetadata(context); + } } diff --git a/app/src/test/java/org/fdroid/fdroid/data/PreferredSignatureTest.java b/app/src/test/java/org/fdroid/fdroid/data/PreferredSignatureTest.java index 20c2d55e3..d86510ef1 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/PreferredSignatureTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/PreferredSignatureTest.java @@ -25,6 +25,14 @@ public class PreferredSignatureTest extends FDroidProviderTest { public void setup() { TestUtils.registerContentProvider(AppProvider.getAuthority(), AppProvider.class); Preferences.setup(context); + + // This is what the FDroidApp does when this preference is changed. Need to also do this under testing. + Preferences.get().registerUnstableUpdatesChangeListener(new Preferences.ChangeListener() { + @Override + public void onPreferenceChange() { + AppProvider.Helper.calcSuggestedApks(context); + } + }); } @After @@ -46,6 +54,8 @@ public class PreferredSignatureTest extends FDroidProviderTest { TestUtils.insertApk(context, app, 2100, TestUtils.UPSTREAM_SIG); // 2.0 TestUtils.insertApk(context, app, 3100, TestUtils.UPSTREAM_SIG); // 3.0 + TestUtils.updateDbAfterInserting(context); + return app; } @@ -70,6 +80,8 @@ public class PreferredSignatureTest extends FDroidProviderTest { TestUtils.insertApk(context, app, 5002, TestUtils.THIRD_PARTY_SIG); // 5.0-rc2 TestUtils.insertApk(context, app, 5003, TestUtils.THIRD_PARTY_SIG); // 5.0-rc3 + TestUtils.updateDbAfterInserting(context); + return app; } @@ -84,6 +96,8 @@ public class PreferredSignatureTest extends FDroidProviderTest { TestUtils.insertApk(context, app, 3100, TestUtils.UPSTREAM_SIG); TestUtils.insertApk(context, app, 4100, TestUtils.UPSTREAM_SIG); + TestUtils.updateDbAfterInserting(context); + return app; } @@ -265,9 +279,6 @@ public class PreferredSignatureTest extends FDroidProviderTest { } private void assertSuggested(Context context, int suggestedVersion, String suggestedSig) { - AppProvider.Helper.calcSuggestedApks(context); - AppProvider.Helper.recalculatePreferredMetadata(context); - App suggestedApp = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), PACKAGE_NAME); assertEquals("Suggested version on App", suggestedVersion, suggestedApp.suggestedVersionCode); diff --git a/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java b/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java index 010e2b638..447bd3301 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java @@ -26,6 +26,14 @@ public class SuggestedVersionTest extends FDroidProviderTest { public void setup() { TestUtils.registerContentProvider(AppProvider.getAuthority(), AppProvider.class); Preferences.setup(context); + + // This is what the FDroidApp does when this preference is changed. Need to also do this under testing. + Preferences.get().registerUnstableUpdatesChangeListener(new Preferences.ChangeListener() { + @Override + public void onPreferenceChange() { + AppProvider.Helper.calcSuggestedApks(context); + } + }); } @After @@ -40,6 +48,7 @@ public class SuggestedVersionTest extends FDroidProviderTest { TestUtils.insertApk(context, singleApp, 1, TestUtils.FDROID_SIG); TestUtils.insertApk(context, singleApp, 2, TestUtils.FDROID_SIG); TestUtils.insertApk(context, singleApp, 3, TestUtils.FDROID_SIG); + TestUtils.updateDbAfterInserting(context); assertSuggested("single.app", 2); // By enabling unstable updates, the "upstreamVersionCode" should get ignored, and we should @@ -59,6 +68,7 @@ public class SuggestedVersionTest extends FDroidProviderTest { TestUtils.insertApk(context, singleApp, 3, TestUtils.FDROID_SIG); TestUtils.insertApk(context, singleApp, 4, TestUtils.UPSTREAM_SIG); TestUtils.insertApk(context, singleApp, 5, TestUtils.UPSTREAM_SIG); + TestUtils.updateDbAfterInserting(context); // Given we aren't installed yet, we don't care which signature. // Just get as close to upstreamVersionCode as possible. @@ -72,6 +82,7 @@ public class SuggestedVersionTest extends FDroidProviderTest { // This adds the "upstreamVersionCode" version of the app, but signed by f-droid. TestUtils.insertApk(context, singleApp, 4, TestUtils.FDROID_SIG); TestUtils.insertApk(context, singleApp, 5, TestUtils.FDROID_SIG); + TestUtils.updateDbAfterInserting(context); assertSuggested("single.app", 4, TestUtils.FDROID_SIG, 1); // Version 5 from F-Droid is not the "upstreamVersionCode", but with beta updates it should @@ -99,6 +110,7 @@ public class SuggestedVersionTest extends FDroidProviderTest { TestUtils.insertApk(context, thirdPartyApp, 4, TestUtils.THIRD_PARTY_SIG); TestUtils.insertApk(context, thirdPartyApp, 5, TestUtils.THIRD_PARTY_SIG); TestUtils.insertApk(context, thirdPartyApp, 6, TestUtils.THIRD_PARTY_SIG); + TestUtils.updateDbAfterInserting(context); // Given we aren't installed yet, we don't care which signature or even which repo. // Just get as close to upstreamVersionCode as possible. @@ -112,6 +124,7 @@ public class SuggestedVersionTest extends FDroidProviderTest { // This adds the "upstreamVersionCode" version of the app, but signed by f-droid. TestUtils.insertApk(context, mainApp, 4, TestUtils.FDROID_SIG); TestUtils.insertApk(context, mainApp, 5, TestUtils.FDROID_SIG); + TestUtils.updateDbAfterInserting(context); assertSuggested("single.app", 4, TestUtils.FDROID_SIG, 1); // Uninstalling the F-Droid build and installing v3 of the third party means we can now go @@ -146,6 +159,7 @@ public class SuggestedVersionTest extends FDroidProviderTest { TestUtils.insertApk(context, mainApp, 5, TestUtils.UPSTREAM_SIG); TestUtils.insertApk(context, mainApp, 6, TestUtils.UPSTREAM_SIG); TestUtils.insertApk(context, mainApp, 7, TestUtils.UPSTREAM_SIG); + TestUtils.updateDbAfterInserting(context); // If the user was to manually install the app, they should be suggested version 7 from upstream... assertSuggested("single.app", 7); @@ -180,9 +194,6 @@ public class SuggestedVersionTest extends FDroidProviderTest { * apk is not checked. */ public void assertSuggested(String packageName, int suggestedVersion, String installedSig, int installedVersion) { - AppProvider.Helper.calcSuggestedApks(context); - AppProvider.Helper.recalculatePreferredMetadata(context); - App suggestedApp = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), packageName); assertEquals("Suggested version on App", suggestedVersion, suggestedApp.suggestedVersionCode); assertEquals("Installed signature on App", installedSig, suggestedApp.installedSig);