diff --git a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java index 7b4d36a5e..a8de65bb9 100644 --- a/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java +++ b/app/src/main/java/org/fdroid/fdroid/IndexV1Updater.java @@ -217,9 +217,14 @@ public class IndexV1Updater extends RepoUpdater { if (packages != null) { apks = packages.get(app.packageName); } + if (apks == null) { Log.i(TAG, "processIndexV1 empty packages"); - apks = new ArrayList(0); + apks = new ArrayList<>(0); + } + + if (apks.size() > 0) { + app.preferredSigner = apks.get(0).sig; } if (appCount % 50 == 0) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java index e73cdfa2e..d3f3e8ddc 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java @@ -75,8 +75,15 @@ public class ApkProvider extends FDroidProvider { return resolver.delete(uri, null, null); } + /** + * Find an app which is closest to the version code suggested by the server, with some caveates: + * + */ public static Apk findSuggestedApk(Context context, App app) { - return findApkFromAnyRepo(context, app.packageName, app.suggestedVersionCode, app.installedSig); + return findApkFromAnyRepo(context, app.packageName, app.suggestedVersionCode, app.getMostAppropriateSignature()); } public static Apk findApkFromAnyRepo(Context context, String packageName, int versionCode) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index ad189b7b0..e8e7621bc 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -98,6 +98,8 @@ public class App extends ValueObject implements Comparable, Parcelable { private long id; @JsonIgnore private AppPrefs prefs; + @JsonIgnore + public String preferredSigner; @JacksonInject("repoId") public long repoId; @@ -286,6 +288,9 @@ public class App extends ValueObject implements Comparable, Parcelable { case Cols.SuggestedApk.VERSION_NAME: suggestedVersionName = cursor.getString(i); break; + case Cols.PREFERRED_SIGNER: + preferredSigner = cursor.getString(i); + break; case Cols.SUGGESTED_VERSION_CODE: suggestedVersionCode = cursor.getInt(i); break; @@ -828,6 +833,7 @@ public class App extends ValueObject implements Comparable, Parcelable { values.put(Cols.FLATTR_ID, flattrID); values.put(Cols.ADDED, Utils.formatDate(added, "")); values.put(Cols.LAST_UPDATED, Utils.formatDate(lastUpdated, "")); + values.put(Cols.PREFERRED_SIGNER, preferredSigner); values.put(Cols.SUGGESTED_VERSION_CODE, suggestedVersionCode); values.put(Cols.UPSTREAM_VERSION_NAME, upstreamVersionName); values.put(Cols.UPSTREAM_VERSION_CODE, upstreamVersionCode); @@ -1003,6 +1009,7 @@ public class App extends ValueObject implements Comparable, Parcelable { dest.writeString(this.bitcoin); dest.writeString(this.litecoin); dest.writeString(this.flattrID); + dest.writeString(this.preferredSigner); dest.writeString(this.upstreamVersionName); dest.writeInt(this.upstreamVersionCode); dest.writeString(this.suggestedVersionName); @@ -1050,6 +1057,7 @@ public class App extends ValueObject implements Comparable, Parcelable { this.bitcoin = in.readString(); this.litecoin = in.readString(); this.flattrID = in.readString(); + this.preferredSigner = in.readString(); this.upstreamVersionName = in.readString(); this.upstreamVersionCode = in.readInt(); this.suggestedVersionName = in.readString(); @@ -1090,4 +1098,24 @@ public class App extends ValueObject implements Comparable, Parcelable { return new App[size]; } }; + + /** + * Choose the signature which we should encourage the user to install. + * Usually, we want the {@link #preferredSigner} rather than any random signature. + * However, if the app is installed, then we override this and instead want to only encourage + * the user to try and install versions with that signature (because thats all the OS will let + * them do). + * TODO: I don't think preferredSigner should ever be null, because if an app has apks then + * we should have chosen the first and used that. If so, then we should change to @NonNull and + * throw an exception if it is null. + */ + @Nullable + public String getMostAppropriateSignature() { + if (!TextUtils.isEmpty(installedSig)) { + return installedSig; + } else if (!TextUtils.isEmpty(preferredSigner)) { + return preferredSigner; + } + return null; + } } 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/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index 555d39f90..3ad0ee818 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -128,6 +128,7 @@ class DBHelper extends SQLiteOpenHelper { + AppMetadataTable.Cols.SOURCE_CODE + " text, " + AppMetadataTable.Cols.VIDEO + " string, " + AppMetadataTable.Cols.CHANGELOG + " text, " + + AppMetadataTable.Cols.PREFERRED_SIGNER + " text," + AppMetadataTable.Cols.SUGGESTED_VERSION_CODE + " text," + AppMetadataTable.Cols.UPSTREAM_VERSION_NAME + " text," + AppMetadataTable.Cols.UPSTREAM_VERSION_CODE + " integer," @@ -192,7 +193,7 @@ class DBHelper extends SQLiteOpenHelper { + InstalledAppTable.Cols.HASH + " TEXT NOT NULL" + " );"; - protected static final int DB_VERSION = 71; + protected static final int DB_VERSION = 72; private final Context context; @@ -278,6 +279,18 @@ class DBHelper extends SQLiteOpenHelper { addWhatsNewAndVideo(db, oldVersion); dropApkPrimaryKey(db, oldVersion); addIntegerPrimaryKeyToInstalledApps(db, oldVersion); + addPreferredSignerToApp(db, oldVersion); + } + + private void addPreferredSignerToApp(SQLiteDatabase db, int oldVersion) { + if (oldVersion >= 72) { + return; + } + + if (!columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.PREFERRED_SIGNER)) { + Log.i(TAG, "Adding preferred signer to app table."); + db.execSQL("alter table " + AppMetadataTable.NAME + " add column " + AppMetadataTable.Cols.PREFERRED_SIGNER + " text;"); + } } private void addIntegerPrimaryKeyToInstalledApps(SQLiteDatabase db, int oldVersion) { 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/main/java/org/fdroid/fdroid/data/Schema.java b/app/src/main/java/org/fdroid/fdroid/data/Schema.java index cb02a85d9..c10996b7c 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Schema.java @@ -136,6 +136,7 @@ public interface Schema { String BITCOIN = "bitcoinAddr"; String LITECOIN = "litecoinAddr"; String FLATTR_ID = "flattrID"; + String PREFERRED_SIGNER = "preferredSigner"; String SUGGESTED_VERSION_CODE = "suggestedVercode"; String UPSTREAM_VERSION_NAME = "upstreamVersion"; String UPSTREAM_VERSION_CODE = "upstreamVercode"; @@ -192,7 +193,7 @@ public interface Schema { ANTI_FEATURES, REQUIREMENTS, ICON_URL, ICON_URL_LARGE, FEATURE_GRAPHIC, PROMO_GRAPHIC, TV_BANNER, PHONE_SCREENSHOTS, SEVEN_INCH_SCREENSHOTS, TEN_INCH_SCREENSHOTS, TV_SCREENSHOTS, WEAR_SCREENSHOTS, - SUGGESTED_VERSION_CODE, + PREFERRED_SIGNER, SUGGESTED_VERSION_CODE, }; /** @@ -208,7 +209,7 @@ public interface Schema { ANTI_FEATURES, REQUIREMENTS, ICON_URL, ICON_URL_LARGE, FEATURE_GRAPHIC, PROMO_GRAPHIC, TV_BANNER, PHONE_SCREENSHOTS, SEVEN_INCH_SCREENSHOTS, TEN_INCH_SCREENSHOTS, TV_SCREENSHOTS, WEAR_SCREENSHOTS, - SUGGESTED_VERSION_CODE, SuggestedApk.VERSION_NAME, + PREFERRED_SIGNER, SUGGESTED_VERSION_CODE, SuggestedApk.VERSION_NAME, InstalledApp.VERSION_CODE, InstalledApp.VERSION_NAME, InstalledApp.SIGNATURE, Package.PACKAGE_NAME, }; diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java index dc76d1f99..46bb5cb39 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java @@ -178,9 +178,10 @@ public class AppDetailsRecyclerViewAdapter private Apk getSuggestedApk() { Apk curApk = null; + String appropriateSig = app.getMostAppropriateSignature(); for (int i = 0; i < versions.size(); i++) { final Apk apk = versions.get(i); - if (apk.versionCode == app.suggestedVersionCode) { + if (apk.versionCode == app.suggestedVersionCode && TextUtils.equals(apk.sig, appropriateSig)) { curApk = apk; break; } @@ -477,7 +478,7 @@ public class AppDetailsRecyclerViewAdapter if (callbacks.isAppDownloading()) { buttonPrimaryView.setText(R.string.downloading); buttonPrimaryView.setEnabled(false); - } else if (!app.isInstalled() && app.suggestedVersionCode > 0 && versions.size() > 0) { + } else if (!app.isInstalled() && suggestedApk != null) { // Check count > 0 due to incompatible apps resulting in an empty list. callbacks.disableAndroidBeam(); // Set Install button and hide second button @@ -486,7 +487,7 @@ public class AppDetailsRecyclerViewAdapter buttonPrimaryView.setEnabled(true); } else if (app.isInstalled()) { callbacks.enableAndroidBeam(); - if (app.canAndWantToUpdate(context)) { + if (app.canAndWantToUpdate(context) && suggestedApk != null) { buttonPrimaryView.setText(R.string.menu_upgrade); buttonPrimaryView.setOnClickListener(onUpgradeClickListener); } else { @@ -818,9 +819,12 @@ public class AppDetailsRecyclerViewAdapter public void bindModel(final Apk apk) { java.text.DateFormat df = DateFormat.getDateFormat(context); + boolean isSuggested = apk.versionCode == app.suggestedVersionCode && + TextUtils.equals(apk.sig, app.getMostAppropriateSignature()); + version.setText(context.getString(R.string.version) + " " + apk.versionName - + (apk.versionCode == app.suggestedVersionCode ? " ☆" : "")); + + (isSuggested ? " ☆" : "")); status.setText(getInstalledStatus(apk)); diff --git a/app/src/test/java/org/fdroid/fdroid/TestUtils.java b/app/src/test/java/org/fdroid/fdroid/TestUtils.java index caf258ea1..d387c7355 100644 --- a/app/src/test/java/org/fdroid/fdroid/TestUtils.java +++ b/app/src/test/java/org/fdroid/fdroid/TestUtils.java @@ -2,9 +2,17 @@ package org.fdroid.fdroid; import android.content.ContentProvider; import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; 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; +import org.fdroid.fdroid.data.Schema; import org.mockito.AdditionalAnswers; import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; @@ -15,7 +23,9 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.security.NoSuchAlgorithmException; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -25,6 +35,85 @@ public class TestUtils { @SuppressWarnings("unused") private static final String TAG = "TestUtils"; // NOPMD + /** + * This is the F-Droid signature used to sign the AdAway binaries used for the multiRepo.*.jar + * repos used by some tests. + */ + public static final String FDROID_CERT = "3082033c30820224a00302010202044e9c4ba6300d06092a864886f70d01010505003060310b300906035504061302554b310c300a060355040813034f5247310c300a060355040713034f524731133011060355040a130a6664726f69642e6f7267310f300d060355040b13064644726f6964310f300d060355040313064644726f6964301e170d3131313031373135333731305a170d3339303330343135333731305a3060310b300906035504061302554b310c300a060355040813034f5247310c300a060355040713034f524731133011060355040a130a6664726f69642e6f7267310f300d060355040b13064644726f6964310f300d060355040313064644726f696430820122300d06092a864886f70d01010105000382010f003082010a0282010100981b0aac96f1c66be3c21e773327ee8c4d3b18c75c548243f4cfedbe8ef0d3c6cc1b3b7b094ddd39cdf71d034ef2cd2d1e7bdca458801b04a531cbe7106a3575151375cb32177b017f81cc508f981a1809d0a417c6f3d59ddfa876c3d91874b1d59e08eaf757da13fb82f7e6f7340abc56f0ab672f02e957d446585931388b1affb6f43a16efc7f060df9c8da17c86899b19495114cc5939decd521e172b48e68c6ec03bc58776acd6a52fd61fd839d2a404df25ae79c2ccec2d9a07c9a1751c341e5e9b706b8e713bec2149e16f5ca15a1d6fe67d52ebb210995ee03d9416118fa9434f65ffe6d43dddfe3e2b0c54b94ea8e5a1031ed41856cd369da41dc6790203010001300d06092a864886f70d0101050500038201010080951aa68b5a2c7ac464b66078afd4826df96e2c10b612a441036e43aa923bfa55f26c61b5d94c2132877a3801c2394328f70b322f6308dbea6ed4f0f4897d73d13af9498277f60685239acd8922275544334d295b07245ef0ec924e1c35e8004d8d268d97c957078149cc5635f8977ce432a56278a03664a45a6be51319b0b5f3e27b2372ae859215e3f3d0f5c8b86d1a42f742abe4d224870d419600966e46d83ce41df04e315353f334378f0f994732a6c05d351b1bea66efc62471762d0f752d379966e8293fc5fe4150665427b0f3fb3a1b64c3b75128abadc02c3efa44c06e2d22ba8f1c3f4b782ac2da0d56307173093fde31215d26ab05714a12d696"; // NOCHECKSTYLE LineLength + public static final String UPSTREAM_CERT = "308204e1308202c9a0030201020204483450fa300d06092a864886f70d01010b050030213110300e060355040b1307462d44726f6964310d300b06035504031304736f7661301e170d3136303832333133333131365a170d3434303130393133333131365a30213110300e060355040b1307462d44726f6964310d300b06035504031304736f766130820222300d06092a864886f70d01010105000382020f003082020a0282020100dfdcd120f3ab224999dddf4ea33ea588d295e4d7130bef48c143e9d76e5c0e0e9e5d45e64208e35feebc79a83f08939dd6a343b7d1e2179930a105a1249ccd36d88ff3feffc6e4dc53dae0163a7876dd45ecc1ddb0adf5099aa56c1a84b52affcd45d0711ffa4de864f35ac0333ebe61ea8673eeda35a88f6af678cc4d0f80b089338ac8f2a8279a64195c611d19445cab3fd1a020afed9bd739bb95142fb2c00a8f847db5ef3325c814f8eb741bacf86ed3907bfe6e4564d2de5895df0c263824e0b75407589bae2d3a4666c13b92102d8781a8ee9bb4a5a1a78c4a9c21efdaf5584da42e84418b28f5a81d0456a3dc5b420991801e6b21e38c99bbe018a5b2d690894a114bc860d35601416aa4dc52216aff8a288d4775cddf8b72d45fd2f87303a8e9c0d67e442530be28eaf139894337266e0b33d57f949256ab32083bcc545bc18a83c9ab8247c12aea037e2b68dee31c734cb1f04f241d3b94caa3a2b258ffaf8e6eae9fbbe029a934dc0a0859c5f120334812693a1c09352340a39f2a678dbc1afa2a978bfee43afefcb7e224a58af2f3d647e5745db59061236b8af6fcfd93b3602f9e456978534f3a7851e800071bf56da80401c81d91c45f82568373af0576b1cc5eef9b85654124b6319770be3cdba3fbebe3715e8918fb6c8966624f3d0e815effac3d2ee06dd34ab9c693218b2c7c06ba99d6b74d4f17b8c3cb0203010001a321301f301d0603551d0e04160414d62bee9f3798509546acc62eb1de14b08b954d4f300d06092a864886f70d01010b05000382020100743f7c5692085895f9d1fffad390fb4202c15f123ed094df259185960fd6dadf66cb19851070f180297bba4e6996a4434616573b375cfee94fee73a4505a7ec29136b7e6c22e6436290e3686fe4379d4e3140ec6a08e70cfd3ed5b634a5eb5136efaaabf5f38e0432d3d79568a556970b8cfba2972f5d23a3856d8a981b9e9bbbbb88f35e708bde9cbc5f681cbd974085b9da28911296fe2579fa64bbe9fa0b93475a7a8db051080b0c5fade0d1c018e7858cd4cbe95145b0620e2f632cbe0f8af9cbf22e2fdaa72245ae31b0877b07181cc69dd2df74454251d8de58d25e76354abe7eb690f22e59b08795a8f2c98c578e0599503d9085927634072c82c9f82abd50fd12b8fd1a9d1954eb5cc0b4cfb5796b5aaec0356643b4a65a368442d92ef94edd3ac6a2b7fe3571b8cf9f462729228aab023ef9183f73792f5379633ccac51079177d604c6bc1873ada6f07d8da6d68c897e88a5fa5d63fdb8df820f46090e0716e7562dd3c140ba279a65b996f60addb0abe29d4bf2f5abe89480771d492307b926d91f02f341b2148502903c43d40f3c6c86a811d060711f0698b384acdcc0add44eb54e42962d3d041accc715afd49407715adc09350cb55e8d9281a3b0b6b5fcd91726eede9b7c8b13afdebb2c2b377629595f1096ba62fb14946dbac5f3c5f0b4e5b712e7acc7dcf6c46cdc5e6d6dfdeee55a0c92c2d70f080ac6"; // NOCHECKSTYLE LineLength + public static final String THIRD_PARTY_CERT = "308204e1308202c9a0030201020204483450fa300d06092a864886f70d01010b050030213110300e060355040b130abcdeabcde012340123400b06035504031304736f7661301e170d3136303832333133333131365a170d3434303130393133333131365a30213110300e060355040b1307462d44726f6964310d300b06035504031304736f766130820222300d06092a864886f70d01010105000382020f003082020a0282020100dfdcd120f3ab224999dddf4ea33ea588d295e4d7130bef48c143e9d76e5c0e0e9e5d45e64208e35feebc79a83f08939dd6a343b7d1e2179930a105a1249ccd36d88ff3feffc6e4dc53dae0163a7876dd45ecc1ddb0adf5099aa56c1a84b52affcd45d0711ffa4de864f35ac0333ebe61ea8673eeda35a88f6af678cc4d0f80b089338ac8f2a8279a64195c611d19445cab3fd1a020afed9bd739bb95142fb2c00a8f847db5ef3325c814f8eb741bacf86ed3907bfe6e4564d2de5895df0c263824e0b75407589bae2d3a4666c13b92102d8781a8ee9bb4a5a1a78c4a9c21efdaf5584da42e84418b28f5a81d0456a3dc5b420991801e6b21e38c99bbe018a5b2d690894a114bc860d35601416aa4dc52216aff8a288d4775cddf8b72d45fd2f87303a8e9c0d67e442530be28eaf139894337266e0b33d57f949256ab32083bcc545bc18a83c9ab8247c12aea037e2b68dee31c734cb1f04f241d3b94caa3a2b258ffaf8e6eae9fbbe029a934dc0a0859c5f120334812693a1c09352340a39f2a678dbc1afa2a978bfee43afefcb7e224a58af2f3d647e5745db59061236b8af6fcfd93b3602f9e456978534f3a7851e800071bf56da80401c81d91c45f82568373af0576b1cc5eef9b85654124b6319770be3cdba3fbebe3715e8918fb6c8966624f3d0e815effac3d2ee06dd34ab9c693218b2c7c06ba99d6b74d4f17b8c3cb0203010001a321301f301d0603551d0e04160414d62bee9f3798509546acc62eb1de14b08b954d4f300d06092a864886f70d01010b05000382020100743f7c5692085895f9d1fffad390fb4202c15f123ed094df259185960fd6dadf66cb19851070f180297bba4e6996a4434616573b375cfee94fee73a4505a7ec29136b7e6c22e6436290e3686fe4379d4e3140ec6a08e70cfd3ed5b634a5eb5136efaaabf5f38e0432d3d79568a556970b8cfba2972f5d23a3856d8a981b9e9bbbbb88f35e708bde9cbc5f681cbd974085b9da28911296fe2579fa64bbe9fa0b93475a7a8db051080b0c5fade0d1c018e7858cd4cbe95145b0620e2f632cbe0f8af9cbf22e2fdaa72245ae31b0877b07181cc69dd2df74454251d8de58d25e76354abe7eb690f22e59b08795a8f2c98c578e0599503d9085927634072c82c9f82abd50fd12b8fd1a9d1954eb5cc0b4cfb5796b5aaec0356643b4a65a368442d92ef94edd3ac6a2b7fe3571b8cf9f462729228aab023ef9183f73792f5379633ccac51079177d604c6bc1873ada6f07d8da6d68c897e88a5fa5d63fdb8df820f46090e0716e7562dd3c140ba279a65b996f60addb0abe29d4bf2f5abe89480771d492307b926d91f02f341b2148502903c43d40f3c6c86a811d060711f0698b384acdcc0add44eb54e42962d3d041accc715afd49407715adc09350cb55e8d9281a3b0b6b5fcd91726eede9b7c8b13afdebb2c2b377629595f1096ba62fb14946dbac5f3c5f0b4e5b712e7acc7dcf6c46cdc5e6d6dfdeee55a0c92c2d70f080ac6"; // NOCHECKSTYLE LineLength + + public static final String FDROID_SIG; + public static final String UPSTREAM_SIG; + public static final String THIRD_PARTY_SIG; + + static { + // Some code requires the full certificate (e.g. when we mock PackageInfo to give to the + // installed app provider), while others requires the hashed certificate (e.g. inserting + // into the apk provider directly, without the need to mock anything). + try { + FDROID_SIG = new Hasher("MD5", FDROID_CERT.getBytes()).getHash(); + UPSTREAM_SIG = new Hasher("MD5", UPSTREAM_CERT.getBytes()).getHash(); + THIRD_PARTY_SIG = new Hasher("MD5", THIRD_PARTY_CERT.getBytes()).getHash(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } + + private static String formatSigForDebugging(String sig) { + String suffix; + + // Can't use a switch statement here because *_SIG is not a constant, despite beign static final. + if (sig.equals(FDROID_SIG)) { + suffix = "F-Droid"; + } else if (sig.equals(UPSTREAM_SIG)) { + suffix = "Upstream"; + } else if (sig.equals(THIRD_PARTY_SIG)) { + suffix = "3rd Party"; + } else { + suffix = "Unknown"; + } + + return sig + " [" + suffix + "]"; + } + + public static void assertSignaturesMatch(String message, String expected, String actual) { + assertEquals(message, formatSigForDebugging(expected), formatSigForDebugging(actual)); + } + + public static void insertApk(Context context, App app, int versionCode, String signature) { + ContentValues values = new ContentValues(); + values.put(Schema.ApkTable.Cols.SIGNATURE, signature); + Assert.insertApk(context, app, versionCode, values); + } + + public static App insertApp(Context context, String packageName, String appName, int upstreamVersionCode, + String repoUrl) { + Repo repo = ensureRepo(context, repoUrl); + ContentValues values = new ContentValues(); + values.put(Schema.AppMetadataTable.Cols.REPO_ID, repo.getId()); + values.put(Schema.AppMetadataTable.Cols.UPSTREAM_VERSION_CODE, upstreamVersionCode); + return Assert.insertApp(context, packageName, appName, values); + } + + public static App insertApp(Context context, String packageName, String appName, int upstreamVersionCode, + Repo repo, String preferredSigner) { + ContentValues values = new ContentValues(); + values.put(Schema.AppMetadataTable.Cols.REPO_ID, repo.getId()); + values.put(Schema.AppMetadataTable.Cols.UPSTREAM_VERSION_CODE, upstreamVersionCode); + values.put(Schema.AppMetadataTable.Cols.PREFERRED_SIGNER, preferredSigner); + return Assert.insertApp(context, packageName, appName, values); + } + + public static Repo ensureRepo(Context context, String repoUrl) { + Repo existing = RepoProvider.Helper.findByAddress(context, repoUrl); + if (existing != null) { + return existing; + } + + return RepoProviderTest.insertRepo(context, repoUrl, "", "", ""); + } + public static void registerContentProvider(String authority, Class providerClass) { ProviderInfo info = new ProviderInfo(); info.authority = authority; @@ -73,4 +162,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/AppProviderTest.java b/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java index b9402e83f..9152ded6a 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java @@ -8,8 +8,10 @@ import android.database.Cursor; import android.net.Uri; import org.fdroid.fdroid.BuildConfig; +import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.TestUtils; import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -22,6 +24,7 @@ import java.util.List; import static org.fdroid.fdroid.Assert.assertContainsOnly; import static org.fdroid.fdroid.Assert.assertResultCount; +import static org.fdroid.fdroid.Assert.insertApk; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -38,6 +41,12 @@ public class AppProviderTest extends FDroidProviderTest { @Before public void setup() { TestUtils.registerContentProvider(AppProvider.getAuthority(), AppProvider.class); + Preferences.setup(context); + } + + @After + public void tearDown() { + Preferences.clearSingletonForTesting(); } /** @@ -88,12 +97,20 @@ public class AppProviderTest extends FDroidProviderTest { private void insertAndInstallApp( String packageName, int installedVercode, int suggestedVercode, boolean ignoreAll, int ignoreVercode) { - ContentValues values = new ContentValues(3); - values.put(Cols.SUGGESTED_VERSION_CODE, suggestedVercode); - App app = insertApp(contentResolver, context, packageName, "App: " + packageName, values); + App app = insertApp(contentResolver, context, packageName, "App: " + packageName, new ContentValues()); AppPrefsProvider.Helper.update(context, app, new AppPrefs(ignoreVercode, ignoreAll)); - InstalledAppTestUtils.install(context, packageName, installedVercode, "v" + installedVercode); + ContentValues certValue = new ContentValues(1); + certValue.put(Schema.ApkTable.Cols.SIGNATURE, TestUtils.FDROID_SIG); + + // Make sure that the relevant apks are also in the DB, or else the `install` method below will + // not be able to correctly calculate the suggested version o the apk. + insertApk(context, packageName, installedVercode, certValue); + if (installedVercode != suggestedVercode) { + insertApk(context, packageName, suggestedVercode, certValue); + } + + InstalledAppTestUtils.install(context, packageName, installedVercode, "v" + installedVercode, TestUtils.FDROID_CERT); } @Test diff --git a/app/src/test/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java b/app/src/test/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java index cd5a3e38a..405967a8a 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java @@ -5,8 +5,10 @@ import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import org.fdroid.fdroid.BuildConfig; +import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.TestUtils; import org.fdroid.fdroid.data.Schema.InstalledAppTable.Cols; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,6 +31,12 @@ public class InstalledAppProviderTest extends FDroidProviderTest { @Before public void setup() { TestUtils.registerContentProvider(InstalledAppProvider.getAuthority(), InstalledAppProvider.class); + Preferences.setup(context); + } + + @After + public void tearDown() { + Preferences.clearSingletonForTesting(); } @Test diff --git a/app/src/test/java/org/fdroid/fdroid/data/InstalledAppTestUtils.java b/app/src/test/java/org/fdroid/fdroid/data/InstalledAppTestUtils.java index 9a9eec115..f4b599ae4 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/InstalledAppTestUtils.java +++ b/app/src/test/java/org/fdroid/fdroid/data/InstalledAppTestUtils.java @@ -21,15 +21,15 @@ public class InstalledAppTestUtils { public static void install(Context context, String packageName, int versionCode, String versionName, - @Nullable String signature) { + @Nullable String signingCert) { PackageInfo info = new PackageInfo(); info.packageName = packageName; info.versionCode = versionCode; info.versionName = versionName; info.applicationInfo = new ApplicationInfo(); info.applicationInfo.publicSourceDir = "/tmp/mock-location"; - if (signature != null) { - info.signatures = new Signature[]{new Signature(signature)}; + if (signingCert != null) { + info.signatures = new Signature[]{new Signature(signingCert)}; } String hashType = "sha256"; String hash = "00112233445566778899aabbccddeeff"; diff --git a/app/src/test/java/org/fdroid/fdroid/data/PreferredSignatureTest.java b/app/src/test/java/org/fdroid/fdroid/data/PreferredSignatureTest.java new file mode 100644 index 000000000..d86510ef1 --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/data/PreferredSignatureTest.java @@ -0,0 +1,290 @@ +package org.fdroid.fdroid.data; + +import android.app.Application; +import android.content.Context; + +import org.fdroid.fdroid.BuildConfig; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; + +@Config(constants = BuildConfig.class, application = Application.class, sdk = 24) +@RunWith(RobolectricTestRunner.class) +public class PreferredSignatureTest extends FDroidProviderTest { + + private static final String PACKAGE_NAME = "app.example.com"; + + @Before + 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 + public void tearDown() { + Preferences.clearSingletonForTesting(); + } + + private Repo createFDroidRepo() { + return RepoProviderTest.insertRepo(context, "https://f-droid.org/fdroid/repo", "", "", ""); + } + + private App populateFDroidRepo(Repo repo) { + App app = TestUtils.insertApp(context, PACKAGE_NAME, "App", 3100, repo, TestUtils.UPSTREAM_SIG); + + TestUtils.insertApk(context, app, 1100, TestUtils.FDROID_SIG); // 1.0 + TestUtils.insertApk(context, app, 2100, TestUtils.FDROID_SIG); // 2.0 + TestUtils.insertApk(context, app, 3100, TestUtils.FDROID_SIG); // 3.0 + + 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; + } + + private Repo createDevRepo() { + return RepoProviderTest.insertRepo(context, "https://dev.upstream.com/fdroid/repo", "", "", ""); + } + + private App populateDevRepo(Repo repo) { + App app = TestUtils.insertApp(context, PACKAGE_NAME, "App", 4100, repo, TestUtils.THIRD_PARTY_SIG); + + TestUtils.insertApk(context, app, 1001, TestUtils.THIRD_PARTY_SIG); // 1.0-rc2 + TestUtils.insertApk(context, app, 1100, TestUtils.THIRD_PARTY_SIG); // 1.0 + TestUtils.insertApk(context, app, 2001, TestUtils.THIRD_PARTY_SIG); // 2.0-rc1 + TestUtils.insertApk(context, app, 2002, TestUtils.THIRD_PARTY_SIG); // 2.0-rc2 + TestUtils.insertApk(context, app, 2100, TestUtils.THIRD_PARTY_SIG); // 2.0 + TestUtils.insertApk(context, app, 3001, TestUtils.THIRD_PARTY_SIG); // 3.0-rc1 + TestUtils.insertApk(context, app, 3100, TestUtils.THIRD_PARTY_SIG); // 3.0 + TestUtils.insertApk(context, app, 4001, TestUtils.THIRD_PARTY_SIG); // 4.0-rc1 + TestUtils.insertApk(context, app, 4002, TestUtils.THIRD_PARTY_SIG); // 4.0-rc2 + TestUtils.insertApk(context, app, 4100, TestUtils.THIRD_PARTY_SIG); // 4.0 + TestUtils.insertApk(context, app, 5001, TestUtils.THIRD_PARTY_SIG); // 5.0-rc1 + 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; + } + + private Repo createUpstreamRepo() { + return RepoProviderTest.insertRepo(context, "https://upstream.com/fdroid/repo", "", "", ""); + } + + private App populateUpstreamRepo(Repo repo) { + App app = TestUtils.insertApp(context, PACKAGE_NAME, "App", 4100, repo, TestUtils.UPSTREAM_SIG); + + TestUtils.insertApk(context, app, 2100, TestUtils.UPSTREAM_SIG); + TestUtils.insertApk(context, app, 3100, TestUtils.UPSTREAM_SIG); + TestUtils.insertApk(context, app, 4100, TestUtils.UPSTREAM_SIG); + + TestUtils.updateDbAfterInserting(context); + + return app; + } + + @Test + public void onlyFDroid() { + populateFDroidRepo(createFDroidRepo()); + assertSuggested(context, 3100, TestUtils.UPSTREAM_SIG); + } + + /** + * @see #assertFdroidThenDev() + */ + @Test + public void fdroidThenDev1() { + Repo fdroid = createFDroidRepo(); + Repo dev = createDevRepo(); + + populateFDroidRepo(fdroid); + populateDevRepo(dev); + + assertFdroidThenDev(); + } + + /** + * @see #assertFdroidThenDev() + */ + @Test + public void fdroidThenDev2() { + Repo fdroid = createFDroidRepo(); + Repo dev = createDevRepo(); + + populateDevRepo(dev); + populateFDroidRepo(fdroid); + + assertFdroidThenDev(); + } + + /** + * Both {@link #fdroidThenDev1()} and {@link #fdroidThenDev2()} add the same repos, with the same priorities and + * the same apps/apks. The only difference is in the order with which they get added to the database. They both + * then delegate here and assert that everything works as expected. The reason for testing like this is to ensure + * that the order of rows in the database has no bearing on the correct suggestions of signatures. + * @see #fdroidThenDev1() + * @see #fdroidThenDev2() + */ + private void assertFdroidThenDev() { + assertSuggested(context, 4100, TestUtils.THIRD_PARTY_SIG); + + Preferences.get().setUnstableUpdates(true); + assertSuggested(context, 5003, TestUtils.THIRD_PARTY_SIG); + + Preferences.get().setUnstableUpdates(false); + assertSuggested(context, 4100, TestUtils.THIRD_PARTY_SIG); + } + + /** + * @see #assertFdroidThenUpstream() + */ + @Test + public void fdroidThenUpstream1() { + Repo fdroid = createFDroidRepo(); + Repo upstream = createUpstreamRepo(); + + populateUpstreamRepo(upstream); + populateFDroidRepo(fdroid); + + assertFdroidThenUpstream(); + } + + /** + * @see #assertFdroidThenUpstream() + */ + @Test + public void fdroidThenUpstream2() { + Repo fdroid = createFDroidRepo(); + Repo upstream = createUpstreamRepo(); + + populateFDroidRepo(fdroid); + populateUpstreamRepo(upstream); + + assertFdroidThenUpstream(); + } + + /** + * @see #fdroidThenUpstream1() + * @see #fdroidThenUpstream2() + * @see #assertFdroidThenDev() + */ + private void assertFdroidThenUpstream() { + assertSuggested(context, 4100, TestUtils.UPSTREAM_SIG); + } + + /** + * @see #assertFdroidThenUpstreamThenDev() + */ + @Test + public void fdroidThenUpstreamThenDev1() { + Repo fdroid = createFDroidRepo(); + Repo upstream = createUpstreamRepo(); + Repo dev = createDevRepo(); + + populateFDroidRepo(fdroid); + populateUpstreamRepo(upstream); + populateDevRepo(dev); + + assertFdroidThenUpstreamThenDev(); + } + + /** + * @see #assertFdroidThenUpstreamThenDev() + */ + @Test + public void fdroidThenUpstreamThenDev2() { + Repo fdroid = createFDroidRepo(); + Repo upstream = createUpstreamRepo(); + Repo dev = createDevRepo(); + + populateDevRepo(dev); + populateUpstreamRepo(upstream); + populateFDroidRepo(fdroid); + + assertFdroidThenUpstreamThenDev(); + } + + /** + * @see #fdroidThenUpstreamThenDev1() + * @see #fdroidThenUpstreamThenDev2() + * @see #assertFdroidThenDev() + */ + private void assertFdroidThenUpstreamThenDev() { + assertSuggested(context, 4100, TestUtils.THIRD_PARTY_SIG); + + Preferences.get().setUnstableUpdates(true); + assertSuggested(context, 5003, TestUtils.THIRD_PARTY_SIG); + + Preferences.get().setUnstableUpdates(false); + assertSuggested(context, 4100, TestUtils.THIRD_PARTY_SIG); + } + + /** + * @see #assertFdroidThenDevThenUpstream() + */ + @Test + public void fdroidThenDevThenUpstream1() { + Repo fdroid = createFDroidRepo(); + Repo dev = createDevRepo(); + Repo upstream = createUpstreamRepo(); + + populateFDroidRepo(fdroid); + populateDevRepo(dev); + populateUpstreamRepo(upstream); + + assertFdroidThenDevThenUpstream(); + } + + /** + * @see #assertFdroidThenDevThenUpstream() + */ + @Test + public void fdroidThenDevThenUpstream2() { + Repo fdroid = createFDroidRepo(); + Repo dev = createDevRepo(); + Repo upstream = createUpstreamRepo(); + + populateFDroidRepo(fdroid); + populateDevRepo(dev); + populateUpstreamRepo(upstream); + + assertFdroidThenDevThenUpstream(); + } + + /** + * @see #fdroidThenDevThenUpstream1() + * @see #fdroidThenDevThenUpstream2() + * @see #assertFdroidThenDev() + */ + private void assertFdroidThenDevThenUpstream() { + assertSuggested(context, 4100, TestUtils.UPSTREAM_SIG); + } + + private void assertSuggested(Context context, int suggestedVersion, String suggestedSig) { + App suggestedApp = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), PACKAGE_NAME); + assertEquals("Suggested version on App", suggestedVersion, suggestedApp.suggestedVersionCode); + + Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(context, suggestedApp); + assertEquals("Version on suggested Apk", suggestedVersion, suggestedApk.versionCode); + TestUtils.assertSignaturesMatch("Signature on suggested Apk", suggestedSig, suggestedApk.sig); + } + +} 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 f283f27d8..447bd3301 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java @@ -1,12 +1,8 @@ package org.fdroid.fdroid.data; import android.app.Application; -import android.content.ContentValues; -import android.content.Context; -import org.fdroid.fdroid.Assert; import org.fdroid.fdroid.BuildConfig; -import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.TestUtils; import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; @@ -17,7 +13,6 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import java.security.NoSuchAlgorithmException; import java.util.Collections; import java.util.List; @@ -27,31 +22,18 @@ import static org.junit.Assert.assertEquals; @RunWith(RobolectricTestRunner.class) public class SuggestedVersionTest extends FDroidProviderTest { - private static final String FDROID_CERT = "308202ed308201d5a003020102020426ffa009300d06092a864886f70d01010b05003027310b300906035504061302444531183016060355040a130f4e4f47415050532050726f6a656374301e170d3132313030363132303533325a170d3337303933303132303533325a3027310b300906035504061302444531183016060355040a130f4e4f47415050532050726f6a65637430820122300d06092a864886f70d01010105000382010f003082010a02820101009a8d2a5336b0eaaad89ce447828c7753b157459b79e3215dc962ca48f58c2cd7650df67d2dd7bda0880c682791f32b35c504e43e77b43c3e4e541f86e35a8293a54fb46e6b16af54d3a4eda458f1a7c8bc1b7479861ca7043337180e40079d9cdccb7e051ada9b6c88c9ec635541e2ebf0842521c3024c826f6fd6db6fd117c74e859d5af4db04448965ab5469b71ce719939a06ef30580f50febf96c474a7d265bb63f86a822ff7b643de6b76e966a18553c2858416cf3309dd24278374bdd82b4404ef6f7f122cec93859351fc6e5ea947e3ceb9d67374fe970e593e5cd05c905e1d24f5a5484f4aadef766e498adf64f7cf04bddd602ae8137b6eea40722d0203010001a321301f301d0603551d0e04160414110b7aa9ebc840b20399f69a431f4dba6ac42a64300d06092a864886f70d01010b0500038201010007c32ad893349cf86952fb5a49cfdc9b13f5e3c800aece77b2e7e0e9c83e34052f140f357ec7e6f4b432dc1ed542218a14835acd2df2deea7efd3fd5e8f1c34e1fb39ec6a427c6e6f4178b609b369040ac1f8844b789f3694dc640de06e44b247afed11637173f36f5886170fafd74954049858c6096308fc93c1bc4dd5685fa7a1f982a422f2a3b36baa8c9500474cf2af91c39cbec1bc898d10194d368aa5e91f1137ec115087c31962d8f76cd120d28c249cf76f4c70f5baa08c70a7234ce4123be080cee789477401965cfe537b924ef36747e8caca62dfefdd1a6288dcb1c4fd2aaa6131a7ad254e9742022cfd597d2ca5c660ce9e41ff537e5a4041e37"; // NOCHECKSTYLE LineLength - private static final String UPSTREAM_CERT = "308204e1308202c9a0030201020204483450fa300d06092a864886f70d01010b050030213110300e060355040b1307462d44726f6964310d300b06035504031304736f7661301e170d3136303832333133333131365a170d3434303130393133333131365a30213110300e060355040b1307462d44726f6964310d300b06035504031304736f766130820222300d06092a864886f70d01010105000382020f003082020a0282020100dfdcd120f3ab224999dddf4ea33ea588d295e4d7130bef48c143e9d76e5c0e0e9e5d45e64208e35feebc79a83f08939dd6a343b7d1e2179930a105a1249ccd36d88ff3feffc6e4dc53dae0163a7876dd45ecc1ddb0adf5099aa56c1a84b52affcd45d0711ffa4de864f35ac0333ebe61ea8673eeda35a88f6af678cc4d0f80b089338ac8f2a8279a64195c611d19445cab3fd1a020afed9bd739bb95142fb2c00a8f847db5ef3325c814f8eb741bacf86ed3907bfe6e4564d2de5895df0c263824e0b75407589bae2d3a4666c13b92102d8781a8ee9bb4a5a1a78c4a9c21efdaf5584da42e84418b28f5a81d0456a3dc5b420991801e6b21e38c99bbe018a5b2d690894a114bc860d35601416aa4dc52216aff8a288d4775cddf8b72d45fd2f87303a8e9c0d67e442530be28eaf139894337266e0b33d57f949256ab32083bcc545bc18a83c9ab8247c12aea037e2b68dee31c734cb1f04f241d3b94caa3a2b258ffaf8e6eae9fbbe029a934dc0a0859c5f120334812693a1c09352340a39f2a678dbc1afa2a978bfee43afefcb7e224a58af2f3d647e5745db59061236b8af6fcfd93b3602f9e456978534f3a7851e800071bf56da80401c81d91c45f82568373af0576b1cc5eef9b85654124b6319770be3cdba3fbebe3715e8918fb6c8966624f3d0e815effac3d2ee06dd34ab9c693218b2c7c06ba99d6b74d4f17b8c3cb0203010001a321301f301d0603551d0e04160414d62bee9f3798509546acc62eb1de14b08b954d4f300d06092a864886f70d01010b05000382020100743f7c5692085895f9d1fffad390fb4202c15f123ed094df259185960fd6dadf66cb19851070f180297bba4e6996a4434616573b375cfee94fee73a4505a7ec29136b7e6c22e6436290e3686fe4379d4e3140ec6a08e70cfd3ed5b634a5eb5136efaaabf5f38e0432d3d79568a556970b8cfba2972f5d23a3856d8a981b9e9bbbbb88f35e708bde9cbc5f681cbd974085b9da28911296fe2579fa64bbe9fa0b93475a7a8db051080b0c5fade0d1c018e7858cd4cbe95145b0620e2f632cbe0f8af9cbf22e2fdaa72245ae31b0877b07181cc69dd2df74454251d8de58d25e76354abe7eb690f22e59b08795a8f2c98c578e0599503d9085927634072c82c9f82abd50fd12b8fd1a9d1954eb5cc0b4cfb5796b5aaec0356643b4a65a368442d92ef94edd3ac6a2b7fe3571b8cf9f462729228aab023ef9183f73792f5379633ccac51079177d604c6bc1873ada6f07d8da6d68c897e88a5fa5d63fdb8df820f46090e0716e7562dd3c140ba279a65b996f60addb0abe29d4bf2f5abe89480771d492307b926d91f02f341b2148502903c43d40f3c6c86a811d060711f0698b384acdcc0add44eb54e42962d3d041accc715afd49407715adc09350cb55e8d9281a3b0b6b5fcd91726eede9b7c8b13afdebb2c2b377629595f1096ba62fb14946dbac5f3c5f0b4e5b712e7acc7dcf6c46cdc5e6d6dfdeee55a0c92c2d70f080ac6"; // NOCHECKSTYLE LineLength - private static final String THIRD_PARTY_CERT = "308204e1308202c9a0030201020204483450fa300d06092a864886f70d01010b050030213110300e060355040b130abcdeabcde012340123400b06035504031304736f7661301e170d3136303832333133333131365a170d3434303130393133333131365a30213110300e060355040b1307462d44726f6964310d300b06035504031304736f766130820222300d06092a864886f70d01010105000382020f003082020a0282020100dfdcd120f3ab224999dddf4ea33ea588d295e4d7130bef48c143e9d76e5c0e0e9e5d45e64208e35feebc79a83f08939dd6a343b7d1e2179930a105a1249ccd36d88ff3feffc6e4dc53dae0163a7876dd45ecc1ddb0adf5099aa56c1a84b52affcd45d0711ffa4de864f35ac0333ebe61ea8673eeda35a88f6af678cc4d0f80b089338ac8f2a8279a64195c611d19445cab3fd1a020afed9bd739bb95142fb2c00a8f847db5ef3325c814f8eb741bacf86ed3907bfe6e4564d2de5895df0c263824e0b75407589bae2d3a4666c13b92102d8781a8ee9bb4a5a1a78c4a9c21efdaf5584da42e84418b28f5a81d0456a3dc5b420991801e6b21e38c99bbe018a5b2d690894a114bc860d35601416aa4dc52216aff8a288d4775cddf8b72d45fd2f87303a8e9c0d67e442530be28eaf139894337266e0b33d57f949256ab32083bcc545bc18a83c9ab8247c12aea037e2b68dee31c734cb1f04f241d3b94caa3a2b258ffaf8e6eae9fbbe029a934dc0a0859c5f120334812693a1c09352340a39f2a678dbc1afa2a978bfee43afefcb7e224a58af2f3d647e5745db59061236b8af6fcfd93b3602f9e456978534f3a7851e800071bf56da80401c81d91c45f82568373af0576b1cc5eef9b85654124b6319770be3cdba3fbebe3715e8918fb6c8966624f3d0e815effac3d2ee06dd34ab9c693218b2c7c06ba99d6b74d4f17b8c3cb0203010001a321301f301d0603551d0e04160414d62bee9f3798509546acc62eb1de14b08b954d4f300d06092a864886f70d01010b05000382020100743f7c5692085895f9d1fffad390fb4202c15f123ed094df259185960fd6dadf66cb19851070f180297bba4e6996a4434616573b375cfee94fee73a4505a7ec29136b7e6c22e6436290e3686fe4379d4e3140ec6a08e70cfd3ed5b634a5eb5136efaaabf5f38e0432d3d79568a556970b8cfba2972f5d23a3856d8a981b9e9bbbbb88f35e708bde9cbc5f681cbd974085b9da28911296fe2579fa64bbe9fa0b93475a7a8db051080b0c5fade0d1c018e7858cd4cbe95145b0620e2f632cbe0f8af9cbf22e2fdaa72245ae31b0877b07181cc69dd2df74454251d8de58d25e76354abe7eb690f22e59b08795a8f2c98c578e0599503d9085927634072c82c9f82abd50fd12b8fd1a9d1954eb5cc0b4cfb5796b5aaec0356643b4a65a368442d92ef94edd3ac6a2b7fe3571b8cf9f462729228aab023ef9183f73792f5379633ccac51079177d604c6bc1873ada6f07d8da6d68c897e88a5fa5d63fdb8df820f46090e0716e7562dd3c140ba279a65b996f60addb0abe29d4bf2f5abe89480771d492307b926d91f02f341b2148502903c43d40f3c6c86a811d060711f0698b384acdcc0add44eb54e42962d3d041accc715afd49407715adc09350cb55e8d9281a3b0b6b5fcd91726eede9b7c8b13afdebb2c2b377629595f1096ba62fb14946dbac5f3c5f0b4e5b712e7acc7dcf6c46cdc5e6d6dfdeee55a0c92c2d70f080ac6"; // NOCHECKSTYLE LineLength - - private static final String FDROID_SIG; - private static final String UPSTREAM_SIG; - private static final String THIRD_PARTY_SIG; - - static { - // Some code requires the full certificate (e.g. when we mock PackageInfo to give to the - // installed app provider), while others requires the hashed certificate (e.g. inserting - // into the apk provider directly, without the need to mock anything). - try { - FDROID_SIG = new Hasher("MD5", FDROID_CERT.getBytes()).getHash(); - UPSTREAM_SIG = new Hasher("MD5", UPSTREAM_CERT.getBytes()).getHash(); - THIRD_PARTY_SIG = new Hasher("MD5", THIRD_PARTY_CERT.getBytes()).getHash(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } - @Before 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 @@ -61,10 +43,12 @@ public class SuggestedVersionTest extends FDroidProviderTest { @Test public void singleRepoSingleSig() { - App singleApp = insertApp(context, "single.app", "Single App (with beta)", 2, "https://beta.simple.repo"); - insertApk(context, singleApp, 1, FDROID_SIG); - insertApk(context, singleApp, 2, FDROID_SIG); - insertApk(context, singleApp, 3, FDROID_SIG); + App singleApp = TestUtils.insertApp( + context, "single.app", "Single App (with beta)", 2, "https://beta.simple.repo"); + 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 @@ -75,15 +59,16 @@ public class SuggestedVersionTest extends FDroidProviderTest { @Test public void singleRepoMultiSig() { - App unrelatedApp = insertApp(context, "noisy.app", "Noisy App", 3, "https://simple.repo"); - insertApk(context, unrelatedApp, 3, FDROID_SIG); + App unrelatedApp = TestUtils.insertApp(context, "noisy.app", "Noisy App", 3, "https://simple.repo"); + TestUtils.insertApk(context, unrelatedApp, 3, TestUtils.FDROID_SIG); - App singleApp = insertApp(context, "single.app", "Single App", 4, "https://simple.repo"); - insertApk(context, singleApp, 1, FDROID_SIG); - insertApk(context, singleApp, 2, FDROID_SIG); - insertApk(context, singleApp, 3, FDROID_SIG); - insertApk(context, singleApp, 4, UPSTREAM_SIG); - insertApk(context, singleApp, 5, UPSTREAM_SIG); + App singleApp = TestUtils.insertApp(context, "single.app", "Single App", 4, "https://simple.repo"); + 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.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. @@ -91,38 +76,41 @@ public class SuggestedVersionTest extends FDroidProviderTest { // Now install v1 with the f-droid signature. In response, we should only suggest // apps with that sig in the future. That is, version 4 from upstream is not considered. - InstalledAppTestUtils.install(context, "single.app", 1, "v1", FDROID_CERT); - assertSuggested("single.app", 3, FDROID_SIG, 1); + InstalledAppTestUtils.install(context, "single.app", 1, "v1", TestUtils.FDROID_CERT); + assertSuggested("single.app", 3, TestUtils.FDROID_SIG, 1); // This adds the "upstreamVersionCode" version of the app, but signed by f-droid. - insertApk(context, singleApp, 4, FDROID_SIG); - insertApk(context, singleApp, 5, FDROID_SIG); - assertSuggested("single.app", 4, FDROID_SIG, 1); + 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 // still become the suggested version now. Preferences.get().setUnstableUpdates(true); - assertSuggested("single.app", 5, FDROID_SIG, 1); + assertSuggested("single.app", 5, TestUtils.FDROID_SIG, 1); } @Test public void multiRepoMultiSig() { - App unrelatedApp = insertApp(context, "noisy.app", "Noisy App", 3, "https://simple.repo"); - insertApk(context, unrelatedApp, 3, FDROID_SIG); + App unrelatedApp = TestUtils.insertApp(context, "noisy.app", "Noisy App", 3, "https://simple.repo"); + TestUtils.insertApk(context, unrelatedApp, 3, TestUtils.FDROID_SIG); - App mainApp = insertApp(context, "single.app", "Single App (Main repo)", 4, "https://main.repo"); - App thirdPartyApp = insertApp(context, "single.app", "Single App (3rd party)", 4, "https://3rd-party.repo"); + App mainApp = TestUtils.insertApp(context, "single.app", "Single App (Main repo)", 4, "https://main.repo"); + App thirdPartyApp = TestUtils.insertApp( + context, "single.app", "Single App (3rd party)", 4, "https://3rd-party.repo"); - insertApk(context, mainApp, 1, FDROID_SIG); - insertApk(context, mainApp, 2, FDROID_SIG); - insertApk(context, mainApp, 3, FDROID_SIG); - insertApk(context, mainApp, 4, UPSTREAM_SIG); - insertApk(context, mainApp, 5, UPSTREAM_SIG); + TestUtils.insertApk(context, mainApp, 1, TestUtils.FDROID_SIG); + TestUtils.insertApk(context, mainApp, 2, TestUtils.FDROID_SIG); + TestUtils.insertApk(context, mainApp, 3, TestUtils.FDROID_SIG); + TestUtils.insertApk(context, mainApp, 4, TestUtils.UPSTREAM_SIG); + TestUtils.insertApk(context, mainApp, 5, TestUtils.UPSTREAM_SIG); - insertApk(context, thirdPartyApp, 3, THIRD_PARTY_SIG); - insertApk(context, thirdPartyApp, 4, THIRD_PARTY_SIG); - insertApk(context, thirdPartyApp, 5, THIRD_PARTY_SIG); - insertApk(context, thirdPartyApp, 6, THIRD_PARTY_SIG); + TestUtils.insertApk(context, thirdPartyApp, 3, TestUtils.THIRD_PARTY_SIG); + 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. @@ -130,46 +118,48 @@ public class SuggestedVersionTest extends FDroidProviderTest { // Now install v1 with the f-droid signature. In response, we should only suggest // apps with that sig in the future. That is, version 4 from upstream is not considered. - InstalledAppTestUtils.install(context, "single.app", 1, "v1", FDROID_CERT); - assertSuggested("single.app", 3, FDROID_SIG, 1); + InstalledAppTestUtils.install(context, "single.app", 1, "v1", TestUtils.FDROID_CERT); + assertSuggested("single.app", 3, TestUtils.FDROID_SIG, 1); // This adds the "upstreamVersionCode" version of the app, but signed by f-droid. - insertApk(context, mainApp, 4, FDROID_SIG); - insertApk(context, mainApp, 5, FDROID_SIG); - assertSuggested("single.app", 4, FDROID_SIG, 1); + 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 // back to suggesting version 4. InstalledAppProviderService.deleteAppFromDb(context, "single.app"); - InstalledAppTestUtils.install(context, "single.app", 3, "v3", THIRD_PARTY_CERT); - assertSuggested("single.app", 4, THIRD_PARTY_SIG, 3); + InstalledAppTestUtils.install(context, "single.app", 3, "v3", TestUtils.THIRD_PARTY_CERT); + assertSuggested("single.app", 4, TestUtils.THIRD_PARTY_SIG, 3); // Version 6 from the 3rd party repo is not the "upstreamVersionCode", but with beta updates // it should still become the suggested version now. Preferences.get().setUnstableUpdates(true); - assertSuggested("single.app", 6, THIRD_PARTY_SIG, 3); + assertSuggested("single.app", 6, TestUtils.THIRD_PARTY_SIG, 3); } /** - * This is specifically for the {@link AppProvider.Helper#findCanUpdate(Context, String[])} method used by - * the {@link org.fdroid.fdroid.UpdateService#showAppUpdatesNotification(List)} method. We need to ensure - * that we don't prompt people to update to the wrong sig after an update. + * This is specifically for the {@link AppProvider.Helper#findCanUpdate(android.content.Context, String[])} + * method used by the {@link org.fdroid.fdroid.UpdateService#showAppUpdatesNotification(List)} method. + * We need to ensure that we don't prompt people to update to the wrong sig after an update. */ @Test public void dontSuggestUpstreamVersions() { // By setting the "upstreamVersionCode" to 0, we are letting F-Droid choose the highest compatible version. - App mainApp = insertApp(context, "single.app", "Single App (Main repo)", 0, "https://main.repo"); + App mainApp = TestUtils.insertApp(context, "single.app", "Single App (Main repo)", 0, "https://main.repo"); - insertApk(context, mainApp, 1, FDROID_SIG); - insertApk(context, mainApp, 2, FDROID_SIG); - insertApk(context, mainApp, 3, FDROID_SIG); - insertApk(context, mainApp, 4, FDROID_SIG); - insertApk(context, mainApp, 5, FDROID_SIG); + TestUtils.insertApk(context, mainApp, 1, TestUtils.FDROID_SIG); + TestUtils.insertApk(context, mainApp, 2, TestUtils.FDROID_SIG); + TestUtils.insertApk(context, mainApp, 3, TestUtils.FDROID_SIG); + TestUtils.insertApk(context, mainApp, 4, TestUtils.FDROID_SIG); + TestUtils.insertApk(context, mainApp, 5, TestUtils.FDROID_SIG); - insertApk(context, mainApp, 4, UPSTREAM_SIG); - insertApk(context, mainApp, 5, UPSTREAM_SIG); - insertApk(context, mainApp, 6, UPSTREAM_SIG); - insertApk(context, mainApp, 7, UPSTREAM_SIG); + TestUtils.insertApk(context, mainApp, 4, TestUtils.UPSTREAM_SIG); + 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); @@ -178,13 +168,13 @@ public class SuggestedVersionTest extends FDroidProviderTest { assertEquals(Collections.EMPTY_LIST, AppProvider.Helper.findCanUpdate(context, Cols.ALL)); // After installing an early F-Droid version, we should then suggest the latest F-Droid version. - InstalledAppTestUtils.install(context, "single.app", 2, "v2", FDROID_CERT); - assertSuggested("single.app", 5, FDROID_SIG, 2); + InstalledAppTestUtils.install(context, "single.app", 2, "v2", TestUtils.FDROID_CERT); + assertSuggested("single.app", 5, TestUtils.FDROID_SIG, 2); // However once we've reached the maximum F-Droid version, then we should not suggest higher versions // with different signatures. InstalledAppProviderService.deleteAppFromDb(context, "single.app"); - InstalledAppTestUtils.install(context, "single.app", 5, "v5", FDROID_CERT); + InstalledAppTestUtils.install(context, "single.app", 5, "v5", TestUtils.FDROID_CERT); assertEquals(Collections.EMPTY_LIST, AppProvider.Helper.findCanUpdate(context, Cols.ALL)); } @@ -204,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); @@ -230,28 +217,4 @@ public class SuggestedVersionTest extends FDroidProviderTest { } } - private void insertApk(Context context, App app, int versionCode, String signature) { - ContentValues values = new ContentValues(); - values.put(Schema.ApkTable.Cols.SIGNATURE, signature); - Assert.insertApk(context, app, versionCode, values); - } - - private App insertApp(Context context, String packageName, String appName, int upstreamVersionCode, - String repoUrl) { - Repo repo = ensureRepo(context, repoUrl); - ContentValues values = new ContentValues(); - values.put(Cols.REPO_ID, repo.getId()); - values.put(Cols.UPSTREAM_VERSION_CODE, upstreamVersionCode); - return Assert.insertApp(context, packageName, appName, values); - } - - private Repo ensureRepo(Context context, String repoUrl) { - Repo existing = RepoProvider.Helper.findByAddress(context, repoUrl); - if (existing != null) { - return existing; - } - - return RepoProviderTest.insertRepo(context, repoUrl, "", "", ""); - } - } diff --git a/app/src/test/java/org/fdroid/fdroid/updater/IndexV1UpdaterTest.java b/app/src/test/java/org/fdroid/fdroid/updater/IndexV1UpdaterTest.java index d44c63292..10675b127 100644 --- a/app/src/test/java/org/fdroid/fdroid/updater/IndexV1UpdaterTest.java +++ b/app/src/test/java/org/fdroid/fdroid/updater/IndexV1UpdaterTest.java @@ -263,6 +263,7 @@ public class IndexV1UpdaterTest extends FDroidProviderTest { "installedSig", "installedVersionCode", "installedVersionName", + "preferredSigner", "prefs", "TAG", }; diff --git a/app/src/test/java/org/fdroid/fdroid/updater/ProperMultiRepoUpdaterTest.java b/app/src/test/java/org/fdroid/fdroid/updater/ProperMultiRepoUpdaterTest.java index 7e776adf6..d887fdb7b 100644 --- a/app/src/test/java/org/fdroid/fdroid/updater/ProperMultiRepoUpdaterTest.java +++ b/app/src/test/java/org/fdroid/fdroid/updater/ProperMultiRepoUpdaterTest.java @@ -7,6 +7,7 @@ import android.util.Log; import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.RepoUpdater; +import org.fdroid.fdroid.TestUtils; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; @@ -246,7 +247,7 @@ public class ProperMultiRepoUpdaterTest extends MultiRepoUpdaterTest { } private void assertCanUpdate(String packageName, int installedVersion, int expectedUpdateVersion) { - InstalledAppTestUtils.install(context, packageName, installedVersion, "v" + installedVersion); + InstalledAppTestUtils.install(context, packageName, installedVersion, "v" + installedVersion, TestUtils.FDROID_CERT); List appsToUpdate = AppProvider.Helper.findCanUpdate(context, AppMetadataTable.Cols.ALL); assertEquals(1, appsToUpdate.size()); assertEquals(installedVersion, appsToUpdate.get(0).installedVersionCode);