diff --git a/app/src/androidTest/java/org/fdroid/fdroid/compat/FileCompatTest.java b/app/src/androidTest/java/org/fdroid/fdroid/compat/FileCompatTest.java index 18a9a1367..e56971a15 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/compat/FileCompatTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/compat/FileCompatTest.java @@ -6,6 +6,7 @@ import android.os.Build; import android.os.Environment; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; +import android.util.Log; import org.fdroid.fdroid.AssetUtils; import org.fdroid.fdroid.data.SanitizedFile; @@ -30,6 +31,8 @@ import static org.junit.Assume.assumeTrue; @RunWith(AndroidJUnit4.class) public class FileCompatTest { + private static final String TAG = "FileCompatTest"; + private SanitizedFile sourceFile; private SanitizedFile destFile; @@ -47,11 +50,11 @@ public class FileCompatTest { @After public void tearDown() { if (!sourceFile.delete()) { - System.out.println("Can't delete " + sourceFile.getAbsolutePath() + "."); + Log.w(TAG, "Can't delete " + sourceFile.getAbsolutePath() + "."); } if (!destFile.delete()) { - System.out.println("Can't delete " + destFile.getAbsolutePath() + "."); + Log.w(TAG, "Can't delete " + destFile.getAbsolutePath() + "."); } } diff --git a/app/src/androidTest/java/org/fdroid/fdroid/net/HttpDownloaderTest.java b/app/src/androidTest/java/org/fdroid/fdroid/net/HttpDownloaderTest.java index 08f218716..944304c55 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/net/HttpDownloaderTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/net/HttpDownloaderTest.java @@ -46,16 +46,12 @@ public class HttpDownloaderTest { final CountDownLatch latch = new CountDownLatch(1); String urlString = "https://f-droid.org/repo/index.jar"; receivedProgress = false; - System.out.println("downloadUninterruptedTestWithProgress: " + urlString); - receivedProgress = false; URL url = new URL(urlString); File destFile = File.createTempFile("dl-", ""); final HttpDownloader httpDownloader = new HttpDownloader(url, destFile); httpDownloader.setListener(new ProgressListener() { @Override public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) { - System.out.println("DownloaderProgressListener.sendProgress " - + sourceUrl + " " + bytesRead + " / " + totalBytes); receivedProgress = true; } }); @@ -118,7 +114,6 @@ public class HttpDownloaderTest { httpDownloader.setListener(new ProgressListener() { @Override public void onProgress(URL sourceUrl, int bytesRead, int totalBytes) { - System.out.println("DownloaderProgressListener.sendProgress " + bytesRead + " / " + totalBytes); receivedProgress = true; latch.countDown(); } diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index d71bf60f8..fa4079a77 100644 --- a/app/src/main/java/org/fdroid/fdroid/Preferences.java +++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java @@ -195,6 +195,10 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh return preferences.getBoolean(PREF_UNSTABLE_UPDATES, DEFAULT_UNSTABLE_UPDATES); } + public void setUnstableUpdates(boolean value) { + preferences.edit().putBoolean(PREF_UNSTABLE_UPDATES, value).apply(); + } + public boolean isKeepingInstallHistory() { return preferences.getBoolean(PREF_KEEP_INSTALL_HISTORY, DEFAULT_KEEP_INSTALL_HISTORY); } diff --git a/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java b/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java index df9b625d1..8a51c1697 100644 --- a/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java +++ b/app/src/main/java/org/fdroid/fdroid/RepoXMLHandler.java @@ -379,9 +379,6 @@ public class RepoXMLHandler extends DefaultHandler { } else { removeRequestedPermission(attributes.getValue("name")); } - } else if ("uses-feature".equals(localName) && curapk != null) { - System.out.println("TODO startElement " + uri + " " + localName + " " + qName); - // TODO } curchars.setLength(0); } 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 92f06faae..b803bc95f 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -250,7 +250,7 @@ public class AppProvider extends FDroidProvider { join( InstalledAppTable.NAME, "installed", - "installed." + InstalledAppTable.Cols.PACKAGE_NAME + " = " + PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME); + "installed." + InstalledAppTable.Cols.PACKAGE_ID + " = " + PackageTable.NAME + "." + PackageTable.Cols.ROW_ID); requiresInstalledTable = true; } } @@ -270,7 +270,7 @@ public class AppProvider extends FDroidProvider { leftJoin( InstalledAppTable.NAME, "installed", - "installed." + InstalledAppTable.Cols.PACKAGE_NAME + " = " + PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME); + "installed." + InstalledAppTable.Cols.PACKAGE_ID + " = " + PackageTable.NAME + "." + PackageTable.Cols.ROW_ID); requiresInstalledTable = true; } } @@ -953,50 +953,49 @@ public class AppProvider extends FDroidProvider { * with the closest version code to that, without going over. * If the app is not compatible at all (i.e. no versions were compatible) * then we take the highest, otherwise we take the highest compatible version. + * If the app is installed, then all apks signed by a different certificate are + * ignored for the purpose of this calculation. + * + * @see #updateSuggestedFromLatest() */ private void updateSuggestedFromUpstream() { - Utils.debugLog(TAG, "Calculating suggested versions for all apps which specify an upstream version code."); + Utils.debugLog(TAG, "Calculating suggested versions for all NON-INSTALLED apps which specify an upstream version code."); final String apk = getApkTableName(); final String app = getTableName(); + final String installed = InstalledAppTable.NAME; final boolean unstableUpdates = Preferences.get().getUnstableUpdates(); String restrictToStable = unstableUpdates ? "" : (apk + "." + ApkTable.Cols.VERSION_CODE + " <= " + app + "." + Cols.UPSTREAM_VERSION_CODE + " AND "); + // 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. + // By adding the extra join, and then joining based on the packageId of this inner app table + // and the app table we are updating, we take into account all apks for this app. + + // The check apk.sig = COALESCE(installed.sig, apk.sig) would ideally be better written as: + // `installedSig IS NULL OR installedSig = apk.sig` + // however that would require a separate sub query for each `installedSig` which is more + // expensive. Using a COALESCE is a less expressive way to write the same thing with only + // a single subquery. + // Also note that the `installedSig IS NULL` is not because there is a `NULL` entry in the + // installed table (this is impossible), but rather because the subselect above returned + // zero rows. String updateSql = "UPDATE " + app + " SET " + Cols.SUGGESTED_VERSION_CODE + " = ( " + " SELECT MAX( " + apk + "." + ApkTable.Cols.VERSION_CODE + " ) " + " FROM " + apk + + " JOIN " + app + " AS appForThisApk ON (appForThisApk." + Cols.ROW_ID + " = " + apk + "." + ApkTable.Cols.APP_ID + ") " + + " LEFT JOIN " + installed + " ON (" + installed + "." + InstalledAppTable.Cols.PACKAGE_ID + " = " + app + "." + Cols.PACKAGE_ID + ") " + " WHERE " + - joinToApksRegardlessOfRepo() + " AND " + + app + "." + Cols.PACKAGE_ID + " = appForThisApk." + Cols.PACKAGE_ID + " AND " + + 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 "; - db().execSQL(updateSql); - } - - /** - * Ensure that when we select a list of {@link ApkTable} rows for which to calculate the - * {@link Cols#SUGGESTED_VERSION_CODE}, that we select all apks belonging to the same package, - * regardless of which repo they come from. We can't just join {@link ApkTable} onto the - * {@link AppMetadataTable}, because the {@link AppMetadataTable} table is specific to a repo. - * - * This is required so that apps always have the highest possible - * {@link Cols#SUGGESTED_VERSION_CODE}, regardless of the repository priorities. Without this, - * then each {@link AppMetadataTable} row will have a different {@link Cols#SUGGESTED_VERSION_CODE} - * depending on which repo it came from. With this, each {@link AppMetadataTable} row has the - * same {@link Cols#SUGGESTED_VERSION_CODE}, even if that version is from a different repo. - */ - private String joinToApksRegardlessOfRepo() { - final String apk = getApkTableName(); - final String app = getTableName(); - - return app + "." + Cols.PACKAGE_ID + " = (" + - " SELECT innerAppName." + Cols.PACKAGE_ID + - " FROM " + app + " as innerAppName " + - " WHERE innerAppName." + Cols.ROW_ID + " = " + apk + "." + ApkTable.Cols.APP_ID + - ") "; + LoggingQuery.execSQL(db(), updateSql); } /** @@ -1006,23 +1005,29 @@ public class AppProvider extends FDroidProvider { * If the suggested version is null, it means that we could not figure it * out from the upstream vercode. In such a case, fall back to the simpler * algorithm as if upstreamVercode was 0. + * + * @see #updateSuggestedFromUpstream() */ private void updateSuggestedFromLatest() { 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; String updateSql = "UPDATE " + app + " SET " + Cols.SUGGESTED_VERSION_CODE + " = ( " + " SELECT MAX( " + apk + "." + ApkTable.Cols.VERSION_CODE + " ) " + " FROM " + apk + + " JOIN " + app + " AS appForThisApk ON (appForThisApk." + Cols.ROW_ID + " = " + apk + "." + ApkTable.Cols.APP_ID + ") " + + " LEFT JOIN " + installed + " ON (" + installed + "." + InstalledAppTable.Cols.PACKAGE_ID + " = " + app + "." + Cols.PACKAGE_ID + ") " + " WHERE " + - joinToApksRegardlessOfRepo() + " AND " + + 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 "; - db().execSQL(updateSql); + LoggingQuery.execSQL(db(), updateSql); } 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 5a8852668..555d39f90 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -108,8 +108,7 @@ class DBHelper extends SQLiteOpenHelper { + ApkTable.Cols.ADDED_DATE + " string, " + ApkTable.Cols.IS_COMPATIBLE + " int not null, " + ApkTable.Cols.INCOMPATIBLE_REASONS + " text, " - + ApkTable.Cols.ANTI_FEATURES + " string, " - + "PRIMARY KEY (" + ApkTable.Cols.APP_ID + ", " + ApkTable.Cols.VERSION_CODE + ", " + ApkTable.Cols.REPO_ID + ")" + + ApkTable.Cols.ANTI_FEATURES + " string" + ");"; static final String CREATE_TABLE_APP_METADATA = "CREATE TABLE " + AppMetadataTable.NAME @@ -183,7 +182,7 @@ class DBHelper extends SQLiteOpenHelper { private static final String CREATE_TABLE_INSTALLED_APP = "CREATE TABLE " + InstalledAppTable.NAME + " ( " - + InstalledAppTable.Cols.PACKAGE_NAME + " TEXT NOT NULL PRIMARY KEY, " + + InstalledAppTable.Cols.PACKAGE_ID + " INT NOT NULL UNIQUE, " + InstalledAppTable.Cols.VERSION_CODE + " INT NOT NULL, " + InstalledAppTable.Cols.VERSION_NAME + " TEXT NOT NULL, " + InstalledAppTable.Cols.APPLICATION_LABEL + " TEXT NOT NULL, " @@ -193,7 +192,7 @@ class DBHelper extends SQLiteOpenHelper { + InstalledAppTable.Cols.HASH + " TEXT NOT NULL" + " );"; - protected static final int DB_VERSION = 69; + protected static final int DB_VERSION = 71; private final Context context; @@ -277,6 +276,40 @@ class DBHelper extends SQLiteOpenHelper { addIndexV1AppFields(db, oldVersion); recalculatePreferredMetadata(db, oldVersion); addWhatsNewAndVideo(db, oldVersion); + dropApkPrimaryKey(db, oldVersion); + addIntegerPrimaryKeyToInstalledApps(db, oldVersion); + } + + private void addIntegerPrimaryKeyToInstalledApps(SQLiteDatabase db, int oldVersion) { + if (oldVersion >= 71) { + return; + } + + Log.i(TAG, "Replacing primary key on installed app table with integer for performance."); + + db.beginTransaction(); + try { + if (tableExists(db, Schema.InstalledAppTable.NAME)) { + db.execSQL("DROP TABLE " + Schema.InstalledAppTable.NAME); + } + + db.execSQL(CREATE_TABLE_INSTALLED_APP); + ensureIndexes(db); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + private void dropApkPrimaryKey(SQLiteDatabase db, int oldVersion) { + if (oldVersion >= 70) { + return; + } + + // versionCode + repo is no longer a valid primary key given a repo can have multiple apks + // with the same versionCode, signed by different certificates. + Log.i(TAG, "Dropping composite primary key on apk table in favour of sqlite's rowid"); + resetTransient(db); } private void addWhatsNewAndVideo(SQLiteDatabase db, int oldVersion) { @@ -1050,9 +1083,11 @@ class DBHelper extends SQLiteOpenHelper { AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ");"); } - Utils.debugLog(TAG, "Ensuring indexes exist for " + InstalledAppTable.NAME); - db.execSQL("CREATE INDEX IF NOT EXISTS installedApp_appId_vercode on " + InstalledAppTable.NAME + " (" + - InstalledAppTable.Cols.PACKAGE_NAME + ", " + InstalledAppTable.Cols.VERSION_CODE + ");"); + if (columnExists(db, InstalledAppTable.NAME, InstalledAppTable.Cols.PACKAGE_ID)) { + Utils.debugLog(TAG, "Ensuring indexes exist for " + InstalledAppTable.NAME); + db.execSQL("CREATE INDEX IF NOT EXISTS installedApp_packageId_vercode on " + InstalledAppTable.NAME + " (" + + InstalledAppTable.Cols.PACKAGE_ID + ", " + InstalledAppTable.Cols.VERSION_CODE + ");"); + } Utils.debugLog(TAG, "Ensuring indexes exist for " + RepoTable.NAME); db.execSQL("CREATE INDEX IF NOT EXISTS repo_id_isSwap on " + RepoTable.NAME + " (" + diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledApp.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledApp.java index ec9aec9fd..20edcd367 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledApp.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledApp.java @@ -24,7 +24,7 @@ public class InstalledApp extends ValueObject { case Schema.InstalledAppTable.Cols._ID: id = cursor.getLong(i); break; - case Schema.InstalledAppTable.Cols.PACKAGE_NAME: + case Schema.InstalledAppTable.Cols.Package.NAME: packageName = cursor.getString(i); break; case Schema.InstalledAppTable.Cols.VERSION_CODE: 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 f7ee7ec66..5b30b2549 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java @@ -10,6 +10,7 @@ import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.support.annotation.Nullable; +import android.text.TextUtils; import android.util.Log; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; @@ -41,7 +42,7 @@ public class InstalledAppProvider extends FDroidProvider { cursor.moveToFirst(); while (!cursor.isAfterLast()) { cachedInfo.put( - cursor.getString(cursor.getColumnIndex(Cols.PACKAGE_NAME)), + cursor.getString(cursor.getColumnIndex(Cols.Package.NAME)), cursor.getLong(cursor.getColumnIndex(Cols.LAST_UPDATE_TIME)) ); cursor.moveToNext(); @@ -136,7 +137,17 @@ public class InstalledAppProvider extends FDroidProvider { } private QuerySelection queryApp(String packageName) { - return new QuerySelection(Cols.PACKAGE_NAME + " = ?", new String[]{packageName}); + return new QuerySelection(Cols.Package.NAME + " = ?", new String[]{packageName}); + } + + private QuerySelection queryAppSubQuery(String packageName) { + String pkg = Schema.PackageTable.NAME; + String subQuery = "(" + + " SELECT " + pkg + "." + Schema.PackageTable.Cols.ROW_ID + + " FROM " + pkg + + " WHERE " + pkg + "." + Schema.PackageTable.Cols.PACKAGE_NAME + " = ?)"; + String query = Cols.PACKAGE_ID + " = " + subQuery; + return new QuerySelection(query, new String[]{packageName}); } private QuerySelection querySearch(String query) { @@ -144,6 +155,26 @@ public class InstalledAppProvider extends FDroidProvider { new String[]{"%" + query + "%"}); } + private static class QueryBuilder extends org.fdroid.fdroid.data.QueryBuilder { + @Override + protected String getRequiredTables() { + String pkg = Schema.PackageTable.NAME; + String installed = InstalledAppTable.NAME; + return installed + " JOIN " + pkg + + " ON (" + pkg + "." + Schema.PackageTable.Cols.ROW_ID + " = " + + installed + "." + Cols.PACKAGE_ID + ")"; + } + + @Override + public void addField(String field) { + if (TextUtils.equals(field, Cols.Package.NAME)) { + appendField(Schema.PackageTable.Cols.PACKAGE_NAME, Schema.PackageTable.NAME, field); + } else { + appendField(field, InstalledAppTable.NAME); + } + } + } + @Override public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) { @@ -170,8 +201,15 @@ public class InstalledAppProvider extends FDroidProvider { throw new UnsupportedOperationException(message); } - Cursor cursor = db().query(getTableName(), projection, - selection.getSelection(), selection.getArgs(), null, null, sortOrder); + QueryBuilder query = new QueryBuilder(); + query.addFields(projection); + if (projection.length == 0) { + query.addField(Cols._ID); + } + query.addSelection(selection); + query.addOrderBy(sortOrder); + + Cursor cursor = db().rawQuery(query.toString(), selection.getArgs()); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } @@ -184,7 +222,7 @@ public class InstalledAppProvider extends FDroidProvider { } QuerySelection query = new QuerySelection(where, whereArgs); - query = query.add(queryApp(uri.getLastPathSegment())); + query = query.add(queryAppSubQuery(uri.getLastPathSegment())); return db().delete(getTableName(), query.getSelection(), query.getArgs()); } @@ -196,9 +234,16 @@ 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); + } + verifyVersionNameNotNull(values); db().replaceOrThrow(getTableName(), null, values); - return getAppUri(values.getAsString(Cols.PACKAGE_NAME)); + return getAppUri(values.getAsString(Cols.Package.NAME)); } /** diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java index aa71c8bc1..d319b4033 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -288,7 +288,7 @@ public class InstalledAppProviderService extends IntentService { static void insertAppIntoDb(Context context, PackageInfo packageInfo, String hashType, String hash) { Uri uri = InstalledAppProvider.getContentUri(); ContentValues contentValues = new ContentValues(); - contentValues.put(InstalledAppTable.Cols.PACKAGE_NAME, packageInfo.packageName); + contentValues.put(InstalledAppTable.Cols.Package.NAME, packageInfo.packageName); contentValues.put(InstalledAppTable.Cols.VERSION_CODE, packageInfo.versionCode); contentValues.put(InstalledAppTable.Cols.VERSION_NAME, packageInfo.versionName); contentValues.put(InstalledAppTable.Cols.APPLICATION_LABEL, 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 9eb40417b..db6731307 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/LoggingQuery.java +++ b/app/src/main/java/org/fdroid/fdroid/data/LoggingQuery.java @@ -66,6 +66,20 @@ final class LoggingQuery { return db.rawQuery(query, queryArgs); } + private void execSQLInternal() { + if (BuildConfig.DEBUG) { + long startTime = System.currentTimeMillis(); + db.execSQL(query); + long queryDuration = System.currentTimeMillis() - startTime; + + if (queryDuration >= SLOW_QUERY_DURATION) { + logSlowQuery(queryDuration); + } + } else { + db.execSQL(query); + } + } + /** * Log the query and its duration to the console. In addition, execute an "EXPLAIN QUERY PLAN" * for the query in question so that the query can be diagnosed (https://sqlite.org/eqp.html) @@ -116,4 +130,8 @@ final class LoggingQuery { public static Cursor query(SQLiteDatabase db, String query, String[] queryBuilderArgs) { return new LoggingQuery(db, query, queryBuilderArgs).rawQuery(); } + + public static void execSQL(SQLiteDatabase db, String sql) { + new LoggingQuery(db, sql, null).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 7ae47d8c5..cb02a85d9 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Schema.java @@ -329,7 +329,7 @@ public interface Schema { interface Cols { String _ID = "rowid as _id"; // Required for CursorLoaders - String PACKAGE_NAME = "appId"; + String PACKAGE_ID = "packageId"; String VERSION_CODE = "versionCode"; String VERSION_NAME = "versionName"; String APPLICATION_LABEL = "applicationLabel"; @@ -338,8 +338,12 @@ public interface Schema { String HASH_TYPE = "hashType"; String HASH = "hash"; + interface Package { + String NAME = "packageName"; + } + String[] ALL = { - _ID, PACKAGE_NAME, VERSION_CODE, VERSION_NAME, APPLICATION_LABEL, + _ID, PACKAGE_ID, Package.NAME, VERSION_CODE, VERSION_NAME, APPLICATION_LABEL, SIGNATURE, LAST_UPDATE_TIME, HASH_TYPE, HASH, }; } diff --git a/app/src/main/java/org/fdroid/fdroid/data/TempApkProvider.java b/app/src/main/java/org/fdroid/fdroid/data/TempApkProvider.java index d5a688022..412688121 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/TempApkProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/TempApkProvider.java @@ -129,7 +129,7 @@ public class TempApkProvider extends ApkProvider { final String memoryDbName = TempAppProvider.DB; db.execSQL(DBHelper.CREATE_TABLE_APK.replaceFirst(Schema.ApkTable.NAME, memoryDbName + "." + getTableName())); db.execSQL(TempAppProvider.copyData(Schema.ApkTable.Cols.ALL_COLS, Schema.ApkTable.NAME, memoryDbName + "." + getTableName())); - db.execSQL("CREATE INDEX IF NOT EXISTS " + memoryDbName + ".apk_vercode on " + getTableName() + " (" + ApkTable.Cols.VERSION_CODE + ");"); + db.execSQL("CREATE INDEX IF NOT EXISTS " + memoryDbName + ".apk_appId on " + getTableName() + " (" + ApkTable.Cols.APP_ID + ");"); db.execSQL("CREATE INDEX IF NOT EXISTS " + memoryDbName + ".apk_compatible ON " + getTableName() + " (" + ApkTable.Cols.IS_COMPATIBLE + ");"); } diff --git a/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java index 3c2ed7d2b..79d61817c 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java @@ -209,11 +209,14 @@ public class TempAppProvider extends AppProvider { private void ensureTempTableDetached(SQLiteDatabase db) { try { + // Ideally we'd ask SQLite if the temp table is attached, but that is not possible. + // Instead, we resort to hackery: + // If the first statement does not throw an exception, then the temp db is attached and the second + // statement will detach the database. + db.rawQuery("SELECT * FROM " + DB + "." + getTableName() + " WHERE 0", null); db.execSQL("DETACH DATABASE " + DB); - } catch (SQLiteException e) { - // We expect that most of the time the database will not exist unless an error occurred - // midway through the last update, The resulting exception is: - // android.database.sqlite.SQLiteException: no such database: temp_update_db (code 1) + } catch (SQLiteException ignored) { + } } diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java index ce19186dd..ad1d84816 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java @@ -278,7 +278,7 @@ public class SwapService extends Service { if (!TextUtils.isEmpty(fingerprint)) { values.put(Schema.RepoTable.Cols.FINGERPRINT, peer.getFingerprint()); } - values.put(Schema.RepoTable.Cols.IN_USE, true); + values.put(Schema.RepoTable.Cols.IN_USE, 1); values.put(Schema.RepoTable.Cols.IS_SWAP, true); Uri uri = RepoProvider.Helper.insert(this, values); repo = RepoProvider.Helper.get(this, uri); diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SelectAppsView.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SelectAppsView.java index 77daa73bd..5f0d7a496 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SelectAppsView.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SelectAppsView.java @@ -142,7 +142,7 @@ public class SelectAppsView extends ListView implements private void toggleAppSelected(int position) { Cursor c = (Cursor) adapter.getItem(position); - String packageName = c.getString(c.getColumnIndex(InstalledAppTable.Cols.PACKAGE_NAME)); + String packageName = c.getString(c.getColumnIndex(InstalledAppTable.Cols.Package.NAME)); if (getState().hasSelectedPackage(packageName)) { getState().deselectPackage(packageName); adapter.updateCheckedIndicatorView(position, false); @@ -176,7 +176,7 @@ public class SelectAppsView extends ListView implements for (int i = 0; i < getCount(); i++) { Cursor c = (Cursor) getItemAtPosition(i); - String packageName = c.getString(c.getColumnIndex(InstalledAppTable.Cols.PACKAGE_NAME)); + String packageName = c.getString(c.getColumnIndex(InstalledAppTable.Cols.Package.NAME)); getState().ensureFDroidSelected(); for (String selected : getState().getAppsToSwap()) { if (TextUtils.equals(packageName, selected)) { @@ -257,7 +257,7 @@ public class SelectAppsView extends ListView implements TextView labelView = (TextView) view.findViewById(R.id.application_label); ImageView iconView = (ImageView) view.findViewById(android.R.id.icon); - String packageName = cursor.getString(cursor.getColumnIndex(InstalledAppTable.Cols.PACKAGE_NAME)); + String packageName = cursor.getString(cursor.getColumnIndex(InstalledAppTable.Cols.Package.NAME)); String appLabel = cursor.getString(cursor.getColumnIndex(InstalledAppTable.Cols.APPLICATION_LABEL)); Drawable icon; diff --git a/app/src/test/java/org/fdroid/fdroid/Assert.java b/app/src/test/java/org/fdroid/fdroid/Assert.java index c0ebe5e3f..a9db602e7 100644 --- a/app/src/test/java/org/fdroid/fdroid/Assert.java +++ b/app/src/test/java/org/fdroid/fdroid/Assert.java @@ -158,7 +158,7 @@ public class Assert { Uri uri = InstalledAppProvider.getAppUri(appId); String[] projection = { - InstalledAppTable.Cols.PACKAGE_NAME, + InstalledAppTable.Cols.Package.NAME, InstalledAppTable.Cols.VERSION_CODE, InstalledAppTable.Cols.VERSION_NAME, InstalledAppTable.Cols.APPLICATION_LABEL, @@ -171,7 +171,7 @@ public class Assert { cursor.moveToFirst(); - assertEquals(appId, cursor.getString(cursor.getColumnIndex(InstalledAppTable.Cols.PACKAGE_NAME))); + assertEquals(appId, cursor.getString(cursor.getColumnIndex(InstalledAppTable.Cols.Package.NAME))); assertEquals(versionCode, cursor.getInt(cursor.getColumnIndex(InstalledAppTable.Cols.VERSION_CODE))); assertEquals(versionName, cursor.getString(cursor.getColumnIndex(InstalledAppTable.Cols.VERSION_NAME))); cursor.close(); @@ -196,11 +196,16 @@ public class Assert { values.putAll(additionalValues); + // Don't hard code to 1, let consumers override it in additionalValues then ask for it back. + int repoId = values.getAsInteger(AppMetadataTable.Cols.REPO_ID); + Uri uri = AppProvider.getContentUri(); context.getContentResolver().insert(uri, values); - return AppProvider.Helper.findSpecificApp(context.getContentResolver(), packageName, 1, - AppMetadataTable.Cols.ALL); + App app = AppProvider.Helper.findSpecificApp(context.getContentResolver(), packageName, + repoId, AppMetadataTable.Cols.ALL); + assertNotNull(app); + return app; } public static App ensureApp(Context context, String packageName) { 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 23872b54d..cd5a3e38a 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java @@ -37,7 +37,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest { assertEquals(foundBefore.size(), 0); ContentValues values = new ContentValues(); - values.put(Cols.PACKAGE_NAME, "org.example.test-app"); + values.put(Cols.Package.NAME, "org.example.test-app"); values.put(Cols.APPLICATION_LABEL, "Test App"); values.put(Cols.VERSION_CODE, 1021); values.put(Cols.VERSION_NAME, "Longhorn"); @@ -56,7 +56,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest { assertEquals(cursor.getCount(), 1); cursor.moveToFirst(); - assertEquals("org.example.test-app", cursor.getString(cursor.getColumnIndex(Cols.PACKAGE_NAME))); + assertEquals("org.example.test-app", cursor.getString(cursor.getColumnIndex(Cols.Package.NAME))); assertEquals("Test App", cursor.getString(cursor.getColumnIndex(Cols.APPLICATION_LABEL))); assertEquals(1021, cursor.getInt(cursor.getColumnIndex(Cols.VERSION_CODE))); assertEquals("Longhorn", cursor.getString(cursor.getColumnIndex(Cols.VERSION_NAME))); @@ -125,7 +125,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest { Uri uri = InstalledAppProvider.getAppUri(packageName); String[] projection = { - Cols.PACKAGE_NAME, + Cols.Package.NAME, Cols.LAST_UPDATE_TIME, }; @@ -133,7 +133,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest { assertNotNull(cursor); assertEquals("App \"" + packageName + "\" not installed", 1, cursor.getCount()); cursor.moveToFirst(); - assertEquals(packageName, cursor.getString(cursor.getColumnIndex(Cols.PACKAGE_NAME))); + assertEquals(packageName, cursor.getString(cursor.getColumnIndex(Cols.Package.NAME))); long lastUpdateTime = cursor.getLong(cursor.getColumnIndex(Cols.LAST_UPDATE_TIME)); assertTrue(lastUpdateTime > 0); assertTrue(lastUpdateTime < System.currentTimeMillis()); @@ -170,7 +170,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest { private ContentValues createContentValues(String appId, int versionCode, String versionNumber) { ContentValues values = new ContentValues(3); if (appId != null) { - values.put(Cols.PACKAGE_NAME, appId); + values.put(Cols.Package.NAME, appId); } values.put(Cols.APPLICATION_LABEL, "Mock app: " + appId); values.put(Cols.VERSION_CODE, versionCode); 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 332adc687..9a9eec115 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/InstalledAppTestUtils.java +++ b/app/src/test/java/org/fdroid/fdroid/data/InstalledAppTestUtils.java @@ -3,6 +3,8 @@ package org.fdroid.fdroid.data; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; +import android.content.pm.Signature; +import android.support.annotation.Nullable; public class InstalledAppTestUtils { @@ -13,12 +15,22 @@ public class InstalledAppTestUtils { public static void install(Context context, String packageName, int versionCode, String versionName) { + install(context, packageName, versionCode, versionName, null); + } + + public static void install(Context context, + String packageName, + int versionCode, String versionName, + @Nullable String signature) { 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)}; + } String hashType = "sha256"; String hash = "00112233445566778899aabbccddeeff"; InstalledAppProviderService.insertAppIntoDb(context, info, hashType, hash); diff --git a/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java b/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java new file mode 100644 index 000000000..55c029541 --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/data/SuggestedVersionTest.java @@ -0,0 +1,212 @@ +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; +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 java.security.NoSuchAlgorithmException; + +import static org.junit.Assert.assertEquals; + +@Config(constants = BuildConfig.class, application = Application.class, sdk = 24) +@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); + } + + @After + public void tearDown() { + Preferences.clearSingletonForTesting(); + } + + @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); + AppProvider.Helper.calcSuggestedApks(context); + + App found2 = findApp(singleApp); + assertEquals(2, found2.suggestedVersionCode); + + // By enabling unstable updates, the "upstreamVersionCode" should get ignored, and we should + // suggest the latest version (3). + Preferences.get().setUnstableUpdates(true); + AppProvider.Helper.calcSuggestedApks(context); + App found3 = findApp(singleApp); + assertEquals(3, found3.suggestedVersionCode); + } + + private App findApp(App app) { + return AppProvider.Helper.findSpecificApp(context.getContentResolver(), app.packageName, app.repoId); + } + + @Test + public void singleRepoMultiSig() { + App unrelatedApp = insertApp(context, "noisy.app", "Noisy App", 3, "https://simple.repo"); + insertApk(context, unrelatedApp, 3, 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); + AppProvider.Helper.calcSuggestedApks(context); + + // Given we aren't installed yet, we don't care which signature. + // Just get as close to upstreamVersionCode as possible. + App suggestUpstream4 = findApp(singleApp); + assertEquals(4, suggestUpstream4.suggestedVersionCode); + + // 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); + AppProvider.Helper.calcSuggestedApks(context); + App suggestFDroid3 = findApp(singleApp); + assertEquals(3, suggestFDroid3.suggestedVersionCode); + + // 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); + AppProvider.Helper.calcSuggestedApks(context); + App suggestFDroid4 = findApp(singleApp); + assertEquals(4, suggestFDroid4.suggestedVersionCode); + + // 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); + AppProvider.Helper.calcSuggestedApks(context); + App suggestFDroid5 = findApp(singleApp); + assertEquals(5, suggestFDroid5.suggestedVersionCode); + } + + private void recalculateMetadata() { + AppProvider.Helper.calcSuggestedApks(context); + AppProvider.Helper.recalculatePreferredMetadata(context); + } + + private App highestPriorityApp(String packageName) { + return AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), packageName); + } + + @Test + public void multiRepoMultiSig() { + App unrelatedApp = insertApp(context, "noisy.app", "Noisy App", 3, "https://simple.repo"); + insertApk(context, unrelatedApp, 3, 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"); + + 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); + + 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); + + recalculateMetadata(); + + // Given we aren't installed yet, we don't care which signature or even which repo. + // Just get as close to upstreamVersionCode as possible. + App suggestAnyVersion4 = highestPriorityApp("single.app"); + assertEquals(4, suggestAnyVersion4.suggestedVersionCode); + + // 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); + recalculateMetadata(); + App suggestFDroid3 = highestPriorityApp("single.app"); + assertEquals(3, suggestFDroid3.suggestedVersionCode); + + // 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); + recalculateMetadata(); + App suggestFDroid4 = highestPriorityApp("single.app"); + assertEquals(4, suggestFDroid4.suggestedVersionCode); + + // 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); + recalculateMetadata(); + suggestAnyVersion4 = highestPriorityApp("single.app"); + assertEquals(4, suggestAnyVersion4.suggestedVersionCode); + + // 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); + recalculateMetadata(); + App suggest3rdParty6 = highestPriorityApp("single.app"); + assertEquals(6, suggest3rdParty6.suggestedVersionCode); + } + + 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 8447be3c2..d44c63292 100644 --- a/app/src/test/java/org/fdroid/fdroid/updater/IndexV1UpdaterTest.java +++ b/app/src/test/java/org/fdroid/fdroid/updater/IndexV1UpdaterTest.java @@ -1,6 +1,7 @@ package org.fdroid.fdroid.updater; import android.support.annotation.NonNull; +import android.text.TextUtils; import android.util.Log; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; @@ -177,7 +178,6 @@ public class IndexV1UpdaterTest extends FDroidProviderTest { Repo.PUSH_REQUEST_ACCEPT_ALWAYS); indexV0Details.apps.size(); - System.out.println("total apps: " + apps.length + " " + indexV0Details.apps.size()); assertEquals(indexV0Details.apps.size(), apps.length); assertEquals(apps.length, packages.size()); @@ -336,13 +336,10 @@ public class IndexV1UpdaterTest extends FDroidProviderTest { fields.remove(field); } if (fields.size() > 0) { - System.out.print(instance.getClass() + " has fields not setup for Jackson: "); - for (String field : fields) { - System.out.print("\"" + field + "\", "); - } - System.out.println("\nRead class javadoc for more info."); + String sb = String.valueOf(instance.getClass()) + " has fields not setup for Jackson: " + + TextUtils.join(", ", fields) + "\nRead class javadoc for more info."; + fail(sb); } - assertEquals(0, fields.size()); } @Test @@ -408,7 +405,6 @@ public class IndexV1UpdaterTest extends FDroidProviderTest { } private Repo parseRepo(ObjectMapper mapper, JsonParser parser) throws IOException { - System.out.println("parseRepo "); parser.nextToken(); parser.nextToken(); ObjectReader repoReader = mapper.readerFor(Repo.class); diff --git a/app/src/test/java/org/fdroid/fdroid/updater/Issue763MultiRepo.java b/app/src/test/java/org/fdroid/fdroid/updater/Issue763MultiRepo.java index 22bab5077..8358613e0 100644 --- a/app/src/test/java/org/fdroid/fdroid/updater/Issue763MultiRepo.java +++ b/app/src/test/java/org/fdroid/fdroid/updater/Issue763MultiRepo.java @@ -38,7 +38,7 @@ public class Issue763MultiRepo extends MultiRepoUpdaterTest { public void setEnabled(Repo repo, boolean enabled) { ContentValues values = new ContentValues(1); - values.put(Schema.RepoTable.Cols.IN_USE, enabled); + values.put(Schema.RepoTable.Cols.IN_USE, enabled ? 1 : 0); RepoProvider.Helper.update(context, repo, values); }