diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 70cecf9cb..eb2332ace 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -100,6 +100,11 @@ android:name="org.fdroid.fdroid.data.AppPrefsProvider" android:exported="false"/> + + packages); @@ -280,6 +280,7 @@ public class RepoXMLHandler extends DefaultHandler { } } else if ("application".equals(localName) && curapp == null) { curapp = new App(); + curapp.repoId = repo.getId(); curapp.packageName = attributes.getValue("", "id"); // To appease the NON NULL constraint in the DB. Usually there is a description, and it diff --git a/app/src/main/java/org/fdroid/fdroid/data/Apk.java b/app/src/main/java/org/fdroid/fdroid/data/Apk.java index 1c74be277..5580196ca 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Apk.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Apk.java @@ -63,7 +63,7 @@ public class Apk extends ValueObject implements Comparable, Parcelable { public String[] incompatibleReasons; /** - * The numeric primary key of the App table, which is used to join apks. + * The numeric primary key of the Metadata table, which is used to join apks. */ public long appId; @@ -91,7 +91,7 @@ public class Apk extends ValueObject implements Comparable, Parcelable { case Cols.FEATURES: features = Utils.parseCommaSeparatedString(cursor.getString(i)); break; - case Cols.App.PACKAGE_NAME: + case Cols.Package.PACKAGE_NAME: packageName = cursor.getString(i); break; case Cols.IS_COMPATIBLE: 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 3c635c35f..138f96132 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/ApkProvider.java @@ -11,6 +11,7 @@ import android.util.Log; import org.fdroid.fdroid.data.Schema.ApkTable; import org.fdroid.fdroid.data.Schema.ApkTable.Cols; import org.fdroid.fdroid.data.Schema.AppMetadataTable; +import org.fdroid.fdroid.data.Schema.PackageTable; import org.fdroid.fdroid.data.Schema.RepoTable; import java.util.ArrayList; @@ -36,10 +37,19 @@ public class ApkProvider extends FDroidProvider { public static void update(Context context, Apk apk) { ContentResolver resolver = context.getContentResolver(); - Uri uri = getApkFromAnyRepoUri(apk.packageName, apk.versionCode); + Uri uri = getApkFromRepoUri(apk); resolver.update(uri, apk.toContentValues(), null, null); } + public static Uri getApkFromRepoUri(Apk apk) { + return getContentUri() + .buildUpon() + .appendPath(PATH_APK_FROM_REPO) + .appendPath(Long.toString(apk.appId)) + .appendPath(Integer.toString(apk.versionCode)) + .build(); + } + public static List cursorToList(Cursor cursor) { int knownApkCount = cursor != null ? cursor.getCount() : 0; List apks = new ArrayList<>(knownApkCount); @@ -70,7 +80,7 @@ public class ApkProvider extends FDroidProvider { * Find all apks for a particular app, but limit it to those originating from the * specified repo. */ - public static List find(Context context, Repo repo, List apps, String[] projection) { + public static List findByUri(Context context, Repo repo, List apps, String[] projection) { ContentResolver resolver = context.getContentResolver(); final Uri uri = getContentUriForApps(repo, apps); Cursor cursor = resolver.query(uri, projection, null, null, null); @@ -171,9 +181,12 @@ public class ApkProvider extends FDroidProvider { private static final int CODE_REPO_APPS = CODE_APKS + 1; protected static final int CODE_REPO_APK = CODE_REPO_APPS + 1; private static final int CODE_APK_ROW_ID = CODE_REPO_APK + 1; + static final int CODE_APK_FROM_ANY_REPO = CODE_APK_ROW_ID + 1; + static final int CODE_APK_FROM_REPO = CODE_APK_FROM_ANY_REPO + 1; private static final String PROVIDER_NAME = "ApkProvider"; - protected static final String PATH_APK = "apk"; + protected static final String PATH_APK_FROM_ANY_REPO = "apk-any-repo"; + protected static final String PATH_APK_FROM_REPO = "apk-from-repo"; private static final String PATH_APKS = "apks"; private static final String PATH_APP = "app"; private static final String PATH_REPO = "repo"; @@ -189,10 +202,11 @@ public class ApkProvider extends FDroidProvider { static { REPO_FIELDS.put(Cols.Repo.VERSION, RepoTable.Cols.VERSION); REPO_FIELDS.put(Cols.Repo.ADDRESS, RepoTable.Cols.ADDRESS); - PACKAGE_FIELDS.put(Cols.App.PACKAGE_NAME, AppMetadataTable.Cols.PACKAGE_NAME); + PACKAGE_FIELDS.put(Cols.Package.PACKAGE_NAME, PackageTable.Cols.PACKAGE_NAME); MATCHER.addURI(getAuthority(), PATH_REPO + "/#", CODE_REPO); - MATCHER.addURI(getAuthority(), PATH_APK + "/#/*", CODE_SINGLE); + MATCHER.addURI(getAuthority(), PATH_APK_FROM_ANY_REPO + "/#/*", CODE_APK_FROM_ANY_REPO); + MATCHER.addURI(getAuthority(), PATH_APK_FROM_REPO + "/#/#", CODE_APK_FROM_REPO); MATCHER.addURI(getAuthority(), PATH_APKS + "/*", CODE_APKS); MATCHER.addURI(getAuthority(), PATH_APP + "/*", CODE_PACKAGE); MATCHER.addURI(getAuthority(), PATH_REPO_APPS + "/#/*", CODE_REPO_APPS); @@ -239,7 +253,7 @@ public class ApkProvider extends FDroidProvider { public static Uri getApkFromAnyRepoUri(String packageName, int versionCode) { return getContentUri() .buildUpon() - .appendPath(PATH_APK) + .appendPath(PATH_APK_FROM_ANY_REPO) .appendPath(Integer.toString(versionCode)) .appendPath(packageName) .build(); @@ -274,7 +288,7 @@ public class ApkProvider extends FDroidProvider { builder.append(','); } final Apk apk = apks.get(i); - builder.append(apk.packageName).append(':').append(apk.versionCode); + builder.append(apk.appId).append(':').append(apk.versionCode); } return builder.toString(); } @@ -317,9 +331,11 @@ public class ApkProvider extends FDroidProvider { protected String getRequiredTables() { final String apk = getTableName(); final String app = getAppTableName(); + final String pkg = PackageTable.NAME; return apk + " AS apk " + - " LEFT JOIN " + app + " AS app ON (app." + AppMetadataTable.Cols.ROW_ID + " = apk." + Cols.APP_ID + ")"; + " LEFT JOIN " + app + " AS app ON (app." + AppMetadataTable.Cols.ROW_ID + " = apk." + Cols.APP_ID + ")" + + " LEFT JOIN " + pkg + " AS pkg ON (pkg." + PackageTable.Cols.ROW_ID + " = app." + AppMetadataTable.Cols.PACKAGE_ID + ")"; } @Override @@ -340,7 +356,7 @@ public class ApkProvider extends FDroidProvider { } private void addPackageField(String field, String alias) { - appendField(field, "app", alias); + appendField(field, "pkg", alias); } private void addRepoField(String field, String alias) { @@ -354,12 +370,7 @@ public class ApkProvider extends FDroidProvider { } private QuerySelection queryPackage(String packageName) { - return queryPackage(packageName, true); - } - - private QuerySelection queryPackage(String packageName, boolean includeTableAlias) { - String alias = includeTableAlias ? "apk." : ""; - final String selection = alias + Cols.APP_ID + " = (" + getAppIdFromPackageNameQuery() + ")"; + final String selection = "pkg." + PackageTable.Cols.PACKAGE_NAME + " = ?"; final String[] args = {packageName}; return new QuerySelection(selection, args); } @@ -370,7 +381,15 @@ public class ApkProvider extends FDroidProvider { private QuerySelection querySingleFromAnyRepo(Uri uri, boolean includeAlias) { String alias = includeAlias ? "apk." : ""; - final String selection = alias + Cols.VERSION_CODE + " = ? and " + alias + Cols.APP_ID + " = (" + getAppIdFromPackageNameQuery() + ")"; + + // TODO: Technically multiple repositories can provide the apk with this version code. + // Therefore, in the very near future we'll need to change from calculating a + // "suggested version code" to a "suggested apk" and join directly onto the apk table. + // This way, we can take into account both repo priorities and signing keys of any + // already installed apks to ensure that the best version is suggested to the user. + // At this point, we may pull back the "wrong" apk in weird edge cases, but the user + // wont be tricked into installing it, as it will (likely) have a different signing key. + final String selection = alias + Cols.VERSION_CODE + " = ? and " + alias + Cols.APP_ID + " IN (" + getMetadataIdFromPackageNameQuery() + ")"; final String[] args = { // First (0th) path segment is the word "apk", // and we are not interested in it. @@ -391,6 +410,21 @@ public class ApkProvider extends FDroidProvider { return new QuerySelection(selection, args); } + /** + * Doesn't prefix column names with table alias. This is so that it can be used in UPDATE + * queries. Note that this lack of table alias prefixes means this can't be used for general + * constraints in a regular select query within {@link ApkProvider} as the queries specify + * aliases for the apk table. + */ + private QuerySelection querySingleWithAppId(Uri uri) { + List path = uri.getPathSegments(); + String appId = path.get(1); + String versionCode = path.get(2); + final String selection = Cols.APP_ID + " = ? AND " + Cols.VERSION_CODE + " = ? "; + final String[] args = {appId, versionCode}; + return new QuerySelection(selection, args); + } + protected QuerySelection queryRepo(long repoId) { return queryRepo(repoId, true); } @@ -403,7 +437,7 @@ public class ApkProvider extends FDroidProvider { } private QuerySelection queryRepoApps(long repoId, String packageNames) { - return queryRepo(repoId).add(AppProvider.queryPackageNames(packageNames, "app." + AppMetadataTable.Cols.PACKAGE_NAME)); + return queryRepo(repoId).add(AppProvider.queryPackageNames(packageNames, "pkg." + PackageTable.Cols.PACKAGE_NAME)); } protected QuerySelection queryApks(String apkKeys) { @@ -422,28 +456,32 @@ public class ApkProvider extends FDroidProvider { StringBuilder sb = new StringBuilder(); for (int i = 0; i < apkDetails.length; i++) { String[] parts = apkDetails[i].split(":"); - String packageName = parts[0]; + String appId = parts[0]; String versionCode = parts[1]; - args[i * 2] = packageName; + args[i * 2] = appId; args[i * 2 + 1] = versionCode; if (i != 0) { sb.append(" OR "); } + sb.append(" ( ") - .append(alias) .append(Cols.APP_ID) - .append(" = (") - .append(getAppIdFromPackageNameQuery()) - .append(") AND ") + .append(" = ? ") + .append(" AND ") .append(alias) .append(Cols.VERSION_CODE) .append(" = ? ) "); } + return new QuerySelection(sb.toString(), args); } - private String getAppIdFromPackageNameQuery() { - return "SELECT " + AppMetadataTable.Cols.ROW_ID + " FROM " + getAppTableName() + " WHERE " + AppMetadataTable.Cols.PACKAGE_NAME + " = ?"; + private String getMetadataIdFromPackageNameQuery() { + return "SELECT m." + AppMetadataTable.Cols.ROW_ID + " " + + "FROM " + AppMetadataTable.NAME + " AS m " + + "JOIN " + PackageTable.NAME + " AS p ON ( " + + " m." + AppMetadataTable.Cols.PACKAGE_ID + " = p." + PackageTable.Cols.ROW_ID + " ) " + + "WHERE p." + PackageTable.Cols.PACKAGE_NAME + " = ?"; } @Override @@ -455,7 +493,7 @@ public class ApkProvider extends FDroidProvider { case CODE_LIST: break; - case CODE_SINGLE: + case CODE_APK_FROM_ANY_REPO: query = query.add(querySingleFromAnyRepo(uri)); break; @@ -535,10 +573,6 @@ public class ApkProvider extends FDroidProvider { query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment()), false)); break; - case CODE_PACKAGE: - query = query.add(queryPackage(uri.getLastPathSegment(), false)); - break; - case CODE_APKS: query = query.add(queryApks(uri.getLastPathSegment(), false)); break; @@ -562,7 +596,7 @@ public class ApkProvider extends FDroidProvider { @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { - if (MATCHER.match(uri) != CODE_SINGLE) { + if (MATCHER.match(uri) != CODE_APK_FROM_REPO) { throw new UnsupportedOperationException("Cannot update anything other than a single apk."); } return performUpdateUnchecked(uri, values, where, whereArgs); @@ -573,7 +607,7 @@ public class ApkProvider extends FDroidProvider { removeFieldsFromOtherTables(values); QuerySelection query = new QuerySelection(where, whereArgs); - query = query.add(querySingleFromAnyRepo(uri, false)); + query = query.add(querySingleWithAppId(uri)); int numRows = db().update(getTableName(), values, query.getSelection(), query.getArgs()); if (!isApplyingBatch()) { 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 cfd6c7c74..611072bf9 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -46,6 +46,14 @@ public class App extends ValueObject implements Comparable, Parcelable { public String packageName = "unknown"; public String name = "Unknown"; + + /** + * This is primarily for the purpose of saving app metadata when parsing an index.xml file. + * At most other times, we don't particularly care which repo an {@link App} object came from. + * It is pretty much transparent, because the metadata will be populated from the repo with + * the highest priority. The UI doesn't care normally _which_ repo provided the metadata. + */ + public long repoId; public String summary = "Unknown application"; public String icon; @@ -147,10 +155,13 @@ public class App extends ValueObject implements Comparable, Parcelable { case Cols.ROW_ID: id = cursor.getLong(i); break; + case Cols.REPO_ID: + repoId = cursor.getLong(i); + break; case Cols.IS_COMPATIBLE: compatible = cursor.getInt(i) == 1; break; - case Cols.PACKAGE_NAME: + case Cols.Package.PACKAGE_NAME: packageName = cursor.getString(i); break; case Cols.NAME: @@ -430,8 +441,9 @@ public class App extends ValueObject implements Comparable, Parcelable { final ContentValues values = new ContentValues(); // Intentionally don't put "ROW_ID" in here, because we don't ever want to change that // primary key generated by sqlite. - values.put(Cols.PACKAGE_NAME, packageName); + values.put(Cols.Package.PACKAGE_NAME, packageName); values.put(Cols.NAME, name); + values.put(Cols.REPO_ID, repoId); values.put(Cols.SUMMARY, summary); values.put(Cols.ICON, icon); values.put(Cols.ICON_URL, iconUrl); @@ -560,6 +572,7 @@ public class App extends ValueObject implements Comparable, Parcelable { dest.writeByte(this.compatible ? (byte) 1 : (byte) 0); dest.writeString(this.packageName); dest.writeString(this.name); + dest.writeLong(this.repoId); dest.writeString(this.summary); dest.writeString(this.icon); dest.writeString(this.description); @@ -596,6 +609,7 @@ public class App extends ValueObject implements Comparable, Parcelable { this.compatible = in.readByte() != 0; this.packageName = in.readString(); this.name = in.readString(); + this.repoId = in.readLong(); this.summary = in.readString(); this.icon = in.readString(); this.description = in.readString(); 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 256884f35..ff9cafa66 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/AppProvider.java @@ -18,6 +18,7 @@ import org.fdroid.fdroid.data.Schema.AppPrefsTable; import org.fdroid.fdroid.data.Schema.AppMetadataTable; import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; import org.fdroid.fdroid.data.Schema.InstalledAppTable; +import org.fdroid.fdroid.data.Schema.PackageTable; import org.fdroid.fdroid.data.Schema.RepoTable; import java.util.ArrayList; @@ -27,6 +28,23 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +/** + * Each app has a bunch of metadata that it associates with a package name (such as org.fdroid.fdroid). + * Multiple repositories can host the same package, and provide different metadata for that app. + * + * As such, it is usually the case that you are interested in an {@link App} which has its metadata + * provided by "the repo with the best priority", rather than "specific repo X". This is important + * when asking for an apk, whereby the preferable way is likely using: + * + * * {@link AppProvider.Helper#findHighestPriorityMetadata(ContentResolver, String)} + * + * rather than: + * + * * {@link AppProvider.Helper#findSpecificApp(ContentResolver, String, long, String[])} + * + * The same can be said of retrieving a list of {@link App} objects, where the metadata for each app + * in the result set should be populated from the repository with the best priority. + */ public class AppProvider extends FDroidProvider { private static final String TAG = "AppProvider"; @@ -120,16 +138,27 @@ public class AppProvider extends FDroidProvider { return categories; } - public static App findByPackageName(ContentResolver resolver, String packageName) { - return findByPackageName(resolver, packageName, Cols.ALL); + public static App findHighestPriorityMetadata(ContentResolver resolver, String packageName) { + final Uri uri = getHighestPriorityMetadataUri(packageName); + return cursorToApp(resolver.query(uri, Cols.ALL, null, null, null)); } - public static App findByPackageName(ContentResolver resolver, String packageName, - String[] projection) { - final Uri uri = getContentUri(packageName); + /** + * Returns an {@link App} with metadata provided by a specific {@code repoId}. Keep in mind + * that most of the time we don't care which repo provides the metadata for a particular app, + * as long as it is the repo with the best priority. In those cases, you should instead use + * {@link AppProvider.Helper#findHighestPriorityMetadata(ContentResolver, String)}. + */ + public static App findSpecificApp(ContentResolver resolver, String packageName, long repoId, + String[] projection) { + final Uri uri = getSpecificAppUri(packageName, repoId); return cursorToApp(resolver.query(uri, projection, null, null, null)); } + public static App findSpecificApp(ContentResolver resolver, String packageName, long repoId) { + return findSpecificApp(resolver, packageName, repoId, Cols.ALL); + } + private static App cursorToApp(Cursor cursor) { App app = null; if (cursor != null) { @@ -155,6 +184,11 @@ public class AppProvider extends FDroidProvider { public static List findCanUpdate(Context context, String[] projection) { return cursorToList(context.getContentResolver().query(AppProvider.getCanUpdateUri(), projection, null, null, null)); } + + public static void recalculatePreferredMetadata(Context context) { + Uri uri = Uri.withAppendedPath(AppProvider.getContentUri(), PATH_CALC_PREFERRED_METADATA); + context.getContentResolver().query(uri, null, null, null, null); + } } /** @@ -254,13 +288,15 @@ public class AppProvider extends FDroidProvider { @Override protected String getRequiredTables() { + final String pkg = PackageTable.NAME; final String app = getTableName(); final String apk = getApkTableName(); final String repo = RepoTable.NAME; - return app + - " LEFT JOIN " + apk + " ON (" + apk + "." + ApkTable.Cols.APP_ID + " = " + app + "." + Cols.ROW_ID + ") " + - " LEFT JOIN " + repo + " ON (" + apk + "." + ApkTable.Cols.REPO_ID + " = " + repo + "." + RepoTable.Cols._ID + ") "; + return pkg + + " JOIN " + app + " ON (" + app + "." + Cols.PACKAGE_ID + " = " + pkg + "." + PackageTable.Cols.ROW_ID + ") " + + " JOIN " + repo + " ON (" + app + "." + Cols.REPO_ID + " = " + repo + "." + RepoTable.Cols._ID + ") " + + " LEFT JOIN " + apk + " ON (" + apk + "." + ApkTable.Cols.APP_ID + " = " + app + "." + Cols.ROW_ID + ") "; } @Override @@ -291,7 +327,7 @@ public class AppProvider extends FDroidProvider { join( InstalledAppTable.NAME, "installed", - "installed." + InstalledAppTable.Cols.PACKAGE_NAME + " = " + getTableName() + "." + Cols.PACKAGE_NAME); + "installed." + InstalledAppTable.Cols.PACKAGE_NAME + " = " + PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME); requiresInstalledTable = true; } } @@ -301,7 +337,7 @@ public class AppProvider extends FDroidProvider { leftJoin( AppPrefsTable.NAME, "prefs", - "prefs." + AppPrefsTable.Cols.PACKAGE_NAME + " = " + getTableName() + "." + Cols.PACKAGE_NAME); + "prefs." + AppPrefsTable.Cols.PACKAGE_NAME + " = " + PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME); requiresLeftJoinToPrefs = true; } } @@ -311,7 +347,7 @@ public class AppProvider extends FDroidProvider { leftJoin( InstalledAppTable.NAME, "installed", - "installed." + InstalledAppTable.Cols.PACKAGE_NAME + " = " + getTableName() + "." + Cols.PACKAGE_NAME); + "installed." + InstalledAppTable.Cols.PACKAGE_NAME + " = " + PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME); requiresInstalledTable = true; } } @@ -319,6 +355,9 @@ public class AppProvider extends FDroidProvider { @Override public void addField(String field) { switch (field) { + case Cols.Package.PACKAGE_NAME: + appendField(PackageTable.Cols.PACKAGE_NAME, PackageTable.NAME, Cols.Package.PACKAGE_NAME); + break; case Cols.SuggestedApk.VERSION_NAME: addSuggestedApkVersionField(); break; @@ -404,11 +443,14 @@ public class AppProvider extends FDroidProvider { private static final String PATH_SEARCH_REPO = "searchRepo"; private static final String PATH_NO_APKS = "noApks"; protected static final String PATH_APPS = "apps"; + protected static final String PATH_SPECIFIC_APP = "app"; private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated"; private static final String PATH_NEWLY_ADDED = "newlyAdded"; private static final String PATH_CATEGORY = "category"; private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex"; private static final String PATH_REPO = "repo"; + private static final String PATH_HIGHEST_PRIORITY = "highestPriority"; + private static final String PATH_CALC_PREFERRED_METADATA = "calcPreferredMetadata"; private static final int CAN_UPDATE = CODE_SINGLE + 1; private static final int INSTALLED = CAN_UPDATE + 1; @@ -422,6 +464,8 @@ public class AppProvider extends FDroidProvider { private static final int SEARCH_REPO = REPO + 1; private static final int SEARCH_INSTALLED = SEARCH_REPO + 1; private static final int SEARCH_CAN_UPDATE = SEARCH_INSTALLED + 1; + private static final int HIGHEST_PRIORITY = SEARCH_CAN_UPDATE + 1; + private static final int CALC_PREFERRED_METADATA = HIGHEST_PRIORITY + 1; static { MATCHER.addURI(getAuthority(), null, CODE_LIST); @@ -437,7 +481,9 @@ public class AppProvider extends FDroidProvider { MATCHER.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE); MATCHER.addURI(getAuthority(), PATH_INSTALLED, INSTALLED); MATCHER.addURI(getAuthority(), PATH_NO_APKS, NO_APKS); - MATCHER.addURI(getAuthority(), "*", CODE_SINGLE); + MATCHER.addURI(getAuthority(), PATH_HIGHEST_PRIORITY + "/*", HIGHEST_PRIORITY); + MATCHER.addURI(getAuthority(), PATH_SPECIFIC_APP + "/#/*", CODE_SINGLE); + MATCHER.addURI(getAuthority(), PATH_CALC_PREFERRED_METADATA, CALC_PREFERRED_METADATA); } public static Uri getContentUri() { @@ -486,6 +532,27 @@ public class AppProvider extends FDroidProvider { return getContentUri(app.packageName); } + /** + * @see AppProvider.Helper#findSpecificApp(ContentResolver, String, long, String[]) for details + * of why you should usually prefer {@link AppProvider#getHighestPriorityMetadataUri(String)} to + * this method. + */ + public static Uri getSpecificAppUri(String packageName, long repoId) { + return getContentUri() + .buildUpon() + .appendPath(PATH_SPECIFIC_APP) + .appendPath(Long.toString(repoId)) + .appendPath(packageName) + .build(); + } + + public static Uri getHighestPriorityMetadataUri(String packageName) { + return getContentUri().buildUpon() + .appendPath(PATH_HIGHEST_PRIORITY) + .appendPath(packageName) + .build(); + } + public static Uri getContentUri(String packageName) { return Uri.withAppendedPath(getContentUri(), packageName); } @@ -590,7 +657,7 @@ public class AppProvider extends FDroidProvider { final String app = getTableName(); final String[] columns = { - app + "." + Cols.PACKAGE_NAME, + PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME, app + "." + Cols.NAME, app + "." + Cols.SUMMARY, app + "." + Cols.DESCRIPTION, @@ -624,9 +691,22 @@ public class AppProvider extends FDroidProvider { return new AppQuerySelection(selection.toString(), selectionKeywords); } - protected AppQuerySelection querySingle(String packageName) { - final String selection = getTableName() + "." + Cols.PACKAGE_NAME + " = ?"; - final String[] args = {packageName}; + protected AppQuerySelection querySingle(String packageName, long repoId) { + final String selection = getTableName() + "." + Cols.REPO_ID + " = ? "; + final String[] args = {Long.toString(repoId)}; + return new AppQuerySelection(selection, args).add(queryPackageName(packageName)); + } + + /** + * Same as {@link AppProvider#querySingle(String, long)} except it is used for the purpose + * of an UPDATE query rather than a SELECT query. This means that it must use a subquery to get + * the {@link Cols.Package#PACKAGE_ID} rather than the join which is already in place for that + * table. The reason is because UPDATE queries cannot include joins in SQLite. + */ + protected AppQuerySelection querySingleForUpdate(String packageName, long repoId) { + final String selection = Cols.PACKAGE_ID + " = (" + getPackageIdFromPackageNameQuery() + + ") AND " + Cols.REPO_ID + " = ? "; + final String[] args = {packageName, Long.toString(repoId)}; return new AppQuerySelection(selection, args); } @@ -645,6 +725,22 @@ public class AppProvider extends FDroidProvider { return new AppQuerySelection(selection, args); } + /** + * Ensures that for each app metadata row with the same package name, only the one from the repo + * with the best priority is represented in the result set. While possible to calculate this + * dynamically each time the query is run, we precalculate it during repo updates for performance. + */ + private AppQuerySelection queryHighestPriority() { + final String selection = PackageTable.NAME + "." + PackageTable.Cols.PREFERRED_METADATA + " = " + getTableName() + "." + Cols.ROW_ID; + return new AppQuerySelection(selection); + } + + private AppQuerySelection queryPackageName(String packageName) { + final String selection = PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME + " = ? "; + final String[] args = {packageName}; + return new AppQuerySelection(selection, args); + } + private AppQuerySelection queryRecentlyUpdated() { final String app = getTableName(); final String lastUpdated = app + "." + Cols.LAST_UPDATED; @@ -691,13 +787,29 @@ public class AppProvider extends FDroidProvider { // Queries which are for the main list of apps should not include swap apps. boolean includeSwap = true; + // It is usually the case that we ask for app(s) for which we don't care what repo is + // responsible for providing them. In that case, we need to populate the metadata with + // that form the repo with the highest priority. + // Whenever we know which repo it is coming from, then it is important that we don't + // delegate to the repo with the highest priority, but rather the specific repo we are + // querying from. + boolean repoIsKnown = false; + switch (MATCHER.match(uri)) { + case CALC_PREFERRED_METADATA: + updatePreferredMetadata(); + return null; + case CODE_LIST: includeSwap = false; break; case CODE_SINGLE: - selection = selection.add(querySingle(uri.getLastPathSegment())); + List pathParts = uri.getPathSegments(); + long repoId = Long.parseLong(pathParts.get(1)); + String packageName = pathParts.get(2); + selection = selection.add(querySingle(packageName, repoId)); + repoIsKnown = true; break; case CAN_UPDATE: @@ -707,6 +819,7 @@ public class AppProvider extends FDroidProvider { case REPO: selection = selection.add(queryRepo(Long.parseLong(uri.getLastPathSegment()))); + repoIsKnown = true; break; case INSTALLED: @@ -733,6 +846,7 @@ public class AppProvider extends FDroidProvider { selection = selection .add(querySearch(uri.getPathSegments().get(2))) .add(queryRepo(Long.parseLong(uri.getPathSegments().get(1)))); + repoIsKnown = true; break; case NO_APKS: @@ -756,11 +870,20 @@ public class AppProvider extends FDroidProvider { includeSwap = false; break; + case HIGHEST_PRIORITY: + selection = selection.add(queryPackageName(uri.getLastPathSegment())); + includeSwap = false; + break; + default: Log.e(TAG, "Invalid URI for app content provider: " + uri); throw new UnsupportedOperationException("Invalid URI for app content provider: " + uri); } + if (!repoIsKnown) { + selection = selection.add(queryHighestPriority()); + } + return runQuery(uri, selection, projection, includeSwap, sortOrder); } @@ -799,44 +922,59 @@ public class AppProvider extends FDroidProvider { @Override public Uri insert(Uri uri, ContentValues values) { + long packageId = PackageProvider.Helper.ensureExists(getContext(), values.getAsString(Cols.Package.PACKAGE_NAME)); + values.remove(Cols.Package.PACKAGE_NAME); + values.put(Cols.PACKAGE_ID, packageId); + db().insertOrThrow(getTableName(), null, values); if (!isApplyingBatch()) { getContext().getContentResolver().notifyChange(uri, null); } - return getContentUri(values.getAsString(Cols.PACKAGE_NAME)); + return getSpecificAppUri(values.getAsString(PackageTable.Cols.PACKAGE_NAME), values.getAsLong(Cols.REPO_ID)); } @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { - QuerySelection query = new QuerySelection(where, whereArgs); - switch (MATCHER.match(uri)) { - - case CALC_APP_DETAILS_FROM_INDEX: - updateAppDetails(); - return 0; - - case CODE_SINGLE: - query = query.add(querySingle(uri.getLastPathSegment())); - break; - - default: - throw new UnsupportedOperationException("Update not supported for " + uri + "."); - + if (MATCHER.match(uri) != CALC_APP_DETAILS_FROM_INDEX) { + throw new UnsupportedOperationException("Update not supported for " + uri + "."); } - int count = db().update(getTableName(), values, query.getSelection(), query.getArgs()); - if (!isApplyingBatch()) { - getContext().getContentResolver().notifyChange(uri, null); - } - return count; + + updateAppDetails(); + return 0; } protected void updateAppDetails() { + updatePreferredMetadata(); updateCompatibleFlags(); updateSuggestedFromUpstream(); updateSuggestedFromLatest(); updateIconUrls(getContext(), db(), getTableName(), getApkTableName()); } + private void updatePreferredMetadata() { + Utils.debugLog(TAG, "Deciding on which metadata should take priority for each package."); + + final String app = getTableName(); + + final String highestPriority = + "SELECT MIN(r." + RepoTable.Cols.PRIORITY + ") " + + "FROM " + RepoTable.NAME + " AS r " + + "JOIN " + getTableName() + " AS m ON (m." + Cols.REPO_ID + " = r." + RepoTable.Cols._ID + ") " + + "WHERE m." + Cols.PACKAGE_ID + " = " + "metadata." + Cols.PACKAGE_ID; + + String updateSql = + "UPDATE " + PackageTable.NAME + " " + + "SET " + PackageTable.Cols.PREFERRED_METADATA + " = ( " + + " SELECT metadata." + Cols.ROW_ID + + " FROM " + app + " AS metadata " + + " JOIN " + RepoTable.NAME + " AS repo ON (metadata." + Cols.REPO_ID + " = repo." + RepoTable.Cols._ID + ") " + + " WHERE metadata." + Cols.PACKAGE_ID + " = " + PackageTable.NAME + "." + PackageTable.Cols.ROW_ID + + " AND repo." + RepoTable.Cols.PRIORITY + " = (" + highestPriority + ")" + + ");"; + + db().execSQL(updateSql); + } + /** * For each app, we want to set the isCompatible flag to 1 if any of the apks we know * about are compatible, and 0 otherwise. diff --git a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java index 36d7e93e8..9c238295d 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/data/DBHelper.java @@ -34,6 +34,7 @@ import android.util.Log; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Schema.ApkTable; +import org.fdroid.fdroid.data.Schema.PackageTable; import org.fdroid.fdroid.data.Schema.AppPrefsTable; import org.fdroid.fdroid.data.Schema.AppMetadataTable; import org.fdroid.fdroid.data.Schema.InstalledAppTable; @@ -50,6 +51,12 @@ class DBHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "fdroid"; + private static final String CREATE_TABLE_PACKAGE = "CREATE TABLE " + PackageTable.NAME + + " ( " + + PackageTable.Cols.PACKAGE_NAME + " text not null, " + + PackageTable.Cols.PREFERRED_METADATA + " integer" + + ");"; + private static final String CREATE_TABLE_REPO = "create table " + RepoTable.NAME + " (" + RepoTable.Cols._ID + " integer primary key, " @@ -97,7 +104,8 @@ class DBHelper extends SQLiteOpenHelper { static final String CREATE_TABLE_APP_METADATA = "CREATE TABLE " + AppMetadataTable.NAME + " ( " - + AppMetadataTable.Cols.PACKAGE_NAME + " text not null, " + + AppMetadataTable.Cols.PACKAGE_ID + " integer not null, " + + AppMetadataTable.Cols.REPO_ID + " integer not null, " + AppMetadataTable.Cols.NAME + " text not null, " + AppMetadataTable.Cols.SUMMARY + " text not null, " + AppMetadataTable.Cols.ICON + " text, " @@ -124,7 +132,7 @@ class DBHelper extends SQLiteOpenHelper { + AppMetadataTable.Cols.IS_COMPATIBLE + " int not null," + AppMetadataTable.Cols.ICON_URL + " text, " + AppMetadataTable.Cols.ICON_URL_LARGE + " text, " - + "primary key(" + AppMetadataTable.Cols.PACKAGE_NAME + "));"; + + "primary key(" + AppMetadataTable.Cols.PACKAGE_ID + ", " + AppMetadataTable.Cols.REPO_ID + "));"; private static final String CREATE_TABLE_APP_PREFS = "CREATE TABLE " + AppPrefsTable.NAME + " ( " @@ -146,7 +154,7 @@ class DBHelper extends SQLiteOpenHelper { + " );"; private static final String DROP_TABLE_INSTALLED_APP = "DROP TABLE " + InstalledAppTable.NAME + ";"; - private static final int DB_VERSION = 62; + private static final int DB_VERSION = 63; private final Context context; @@ -248,6 +256,7 @@ class DBHelper extends SQLiteOpenHelper { @Override public void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_PACKAGE); db.execSQL(CREATE_TABLE_APP_METADATA); db.execSQL(CREATE_TABLE_APK); db.execSQL(CREATE_TABLE_INSTALLED_APP); @@ -344,6 +353,63 @@ class DBHelper extends SQLiteOpenHelper { addAppPrefsTable(db, oldVersion); lowerCaseApkHashes(db, oldVersion); supportRepoPushRequests(db, oldVersion); + migrateToPackageTable(db, oldVersion); + } + + private void migrateToPackageTable(SQLiteDatabase db, int oldVersion) { + if (oldVersion >= 63) { + return; + } + + resetTransient(db); + + // By pushing _ALL_ repositories to a priority of 10, it makes it slightly easier + // to query for the non-default repositories later on in this method. + ContentValues highPriority = new ContentValues(1); + highPriority.put(RepoTable.Cols.PRIORITY, 10); + db.update(RepoTable.NAME, highPriority, null, null); + + String[] defaultRepos = context.getResources().getStringArray(R.array.default_repos); + String fdroidPubKey = defaultRepos[7]; + String fdroidAddress = defaultRepos[1]; + String fdroidArchiveAddress = defaultRepos[REPO_XML_ARG_COUNT + 1]; + String gpPubKey = defaultRepos[REPO_XML_ARG_COUNT * 2 + 7]; + String gpAddress = defaultRepos[REPO_XML_ARG_COUNT * 2 + 1]; + String gpArchiveAddress = defaultRepos[REPO_XML_ARG_COUNT * 3 + 1]; + + updateRepoPriority(db, fdroidPubKey, fdroidAddress, 1); + updateRepoPriority(db, fdroidPubKey, fdroidArchiveAddress, 2); + updateRepoPriority(db, gpPubKey, gpAddress, 3); + updateRepoPriority(db, gpPubKey, gpArchiveAddress, 4); + + int priority = 5; + String[] projection = new String[] {RepoTable.Cols.SIGNING_CERT, RepoTable.Cols.ADDRESS}; + + // Order by ID, because that is a good analogy for the order in which they were added. + // The order in which they were added is likely the order they present in the ManageRepos activity. + Cursor cursor = db.query(RepoTable.NAME, projection, RepoTable.Cols.PRIORITY + " > 4", null, null, null, RepoTable.Cols._ID); + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + String signingCert = cursor.getString(cursor.getColumnIndex(RepoTable.Cols.SIGNING_CERT)); + String address = cursor.getString(cursor.getColumnIndex(RepoTable.Cols.ADDRESS)); + updateRepoPriority(db, signingCert, address, priority); + cursor.moveToNext(); + priority++; + } + cursor.close(); + } + + private void updateRepoPriority(SQLiteDatabase db, String signingCert, String address, int priority) { + ContentValues values = new ContentValues(1); + values.put(RepoTable.Cols.PRIORITY, Integer.toString(priority)); + + Utils.debugLog(TAG, "Setting priority of repo " + address + " to " + priority); + db.update( + RepoTable.NAME, + values, + RepoTable.Cols.SIGNING_CERT + " = ? AND " + RepoTable.Cols.ADDRESS + " = ?", + new String[] {signingCert, address} + ); } private void lowerCaseApkHashes(SQLiteDatabase db, int oldVersion) { @@ -370,7 +436,7 @@ class DBHelper extends SQLiteOpenHelper { + AppPrefsTable.Cols.IGNORE_THIS_UPDATE + ", " + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + ") SELECT " - + AppMetadataTable.Cols.PACKAGE_NAME + ", " + + "id, " + "ignoreThisUpdate, " + "ignoreAllUpdates " + "FROM " + AppMetadataTable.NAME + " " @@ -479,7 +545,7 @@ class DBHelper extends SQLiteOpenHelper { final String update = "UPDATE " + ApkTable.NAME + " SET " + ApkTable.Cols.APP_ID + " = ( " + "SELECT app." + AppMetadataTable.Cols.ROW_ID + " " + "FROM " + AppMetadataTable.NAME + " AS app " + - "WHERE " + ApkTable.NAME + ".id = app." + AppMetadataTable.Cols.PACKAGE_NAME + ")"; + "WHERE " + ApkTable.NAME + ".id = app.id)"; Log.i(TAG, "Updating foreign key from " + ApkTable.NAME + " to " + AppMetadataTable.NAME + " to use numeric foreign key."); Utils.debugLog(TAG, update); db.execSQL(update); @@ -733,8 +799,13 @@ class DBHelper extends SQLiteOpenHelper { db.beginTransaction(); try { + if (tableExists(db, PackageTable.NAME)) { + db.execSQL("DROP TABLE " + PackageTable.NAME); + } + db.execSQL("DROP TABLE " + AppMetadataTable.NAME); db.execSQL("DROP TABLE " + ApkTable.NAME); + db.execSQL(CREATE_TABLE_PACKAGE); db.execSQL(CREATE_TABLE_APP_METADATA); db.execSQL(CREATE_TABLE_APK); clearRepoEtags(db); @@ -765,11 +836,24 @@ class DBHelper extends SQLiteOpenHelper { } private static void ensureIndexes(SQLiteDatabase db) { + if (tableExists(db, PackageTable.NAME)) { + Utils.debugLog(TAG, "Ensuring indexes exist for " + PackageTable.NAME); + db.execSQL("CREATE INDEX IF NOT EXISTS package_packageName on " + PackageTable.NAME + " (" + PackageTable.Cols.PACKAGE_NAME + ");"); + db.execSQL("CREATE INDEX IF NOT EXISTS package_preferredMetadata on " + PackageTable.NAME + " (" + PackageTable.Cols.PREFERRED_METADATA + ");"); + } + Utils.debugLog(TAG, "Ensuring indexes exist for " + AppMetadataTable.NAME); - db.execSQL("CREATE INDEX IF NOT EXISTS app_id on " + AppMetadataTable.NAME + " (" + AppMetadataTable.Cols.PACKAGE_NAME + ");"); db.execSQL("CREATE INDEX IF NOT EXISTS name on " + AppMetadataTable.NAME + " (" + AppMetadataTable.Cols.NAME + ");"); // Used for sorting most lists db.execSQL("CREATE INDEX IF NOT EXISTS added on " + AppMetadataTable.NAME + " (" + AppMetadataTable.Cols.ADDED + ");"); // Used for sorting "newly added" + if (columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.PACKAGE_ID)) { + db.execSQL("CREATE INDEX IF NOT EXISTS metadata_packageId ON " + AppMetadataTable.NAME + " (" + AppMetadataTable.Cols.PACKAGE_ID + ");"); + } + + if (columnExists(db, AppMetadataTable.NAME, AppMetadataTable.Cols.REPO_ID)) { + db.execSQL("CREATE INDEX IF NOT EXISTS metadata_repoId ON " + AppMetadataTable.NAME + " (" + AppMetadataTable.Cols.REPO_ID + ");"); + } + Utils.debugLog(TAG, "Ensuring indexes exist for " + ApkTable.NAME); db.execSQL("CREATE INDEX IF NOT EXISTS apk_vercode on " + ApkTable.NAME + " (" + ApkTable.Cols.VERSION_CODE + ");"); db.execSQL("CREATE INDEX IF NOT EXISTS apk_appId on " + ApkTable.NAME + " (" + ApkTable.Cols.APP_ID + ");"); diff --git a/app/src/main/java/org/fdroid/fdroid/data/FDroidProvider.java b/app/src/main/java/org/fdroid/fdroid/data/FDroidProvider.java index c1c046be7..8c341264d 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/FDroidProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/FDroidProvider.java @@ -157,4 +157,12 @@ public abstract class FDroidProvider extends ContentProvider { } } } + + /** + * Helper function to be used when you need to know the primary key from the package table + * when all you have is the package name. + */ + protected static String getPackageIdFromPackageNameQuery() { + return "SELECT " + Schema.PackageTable.Cols.ROW_ID + " FROM " + Schema.PackageTable.NAME + " WHERE " + Schema.PackageTable.Cols.PACKAGE_NAME + " = ?"; + } } diff --git a/app/src/main/java/org/fdroid/fdroid/data/PackageProvider.java b/app/src/main/java/org/fdroid/fdroid/data/PackageProvider.java new file mode 100644 index 000000000..a12be32c5 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/data/PackageProvider.java @@ -0,0 +1,170 @@ +package org.fdroid.fdroid.data; + +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.net.Uri; + +import org.fdroid.fdroid.data.Schema.PackageTable; +import org.fdroid.fdroid.data.Schema.PackageTable.Cols; + +public class PackageProvider extends FDroidProvider { + + public static final class Helper { + private Helper() { } + + public static long ensureExists(Context context, String packageName) { + long id = getPackageId(context, packageName); + if (id <= 0) { + ContentValues values = new ContentValues(1); + values.put(Cols.PACKAGE_NAME, packageName); + Uri uri = context.getContentResolver().insert(getContentUri(), values); + id = Long.parseLong(uri.getLastPathSegment()); + } + return id; + } + + public static long getPackageId(Context context, String packageName) { + String[] projection = new String[] {Cols.ROW_ID}; + Cursor cursor = context.getContentResolver().query(getPackageUri(packageName), projection, null, null, null); + if (cursor == null) { + return 0; + } + + try { + if (cursor.getCount() == 0) { + return 0; + } else { + cursor.moveToFirst(); + return cursor.getLong(cursor.getColumnIndexOrThrow(Cols.ROW_ID)); + } + } finally { + cursor.close(); + } + } + } + + private class Query extends QueryBuilder { + + @Override + protected String getRequiredTables() { + return PackageTable.NAME; + } + + @Override + public void addField(String field) { + appendField(field, getTableName()); + } + } + + private static final String PROVIDER_NAME = "PackageProvider"; + + private static final UriMatcher MATCHER = new UriMatcher(-1); + + private static final String PATH_PACKAGE_NAME = "packageName"; + private static final String PATH_PACKAGE_ID = "packageId"; + + static { + MATCHER.addURI(getAuthority(), PATH_PACKAGE_NAME + "/*", CODE_SINGLE); + } + + private static Uri getContentUri() { + return Uri.parse("content://" + getAuthority()); + } + + public static Uri getPackageUri(String packageName) { + return getContentUri() + .buildUpon() + .appendPath(PATH_PACKAGE_NAME) + .appendPath(packageName) + .build(); + } + + /** + * Not actually used as part of the external API to this content provider. + * Rather, used as a mechanism for returning the ID of a newly inserted row after calling + * {@link android.content.ContentProvider#insert(Uri, ContentValues)}, as that is only able + * to return a {@link Uri}. The {@link Uri#getLastPathSegment()} of this URI contains a + * {@link Long} which is the {@link PackageTable.Cols#ROW_ID} of the newly inserted row. + */ + private static Uri getPackageIdUri(long packageId) { + return getContentUri() + .buildUpon() + .appendPath(PATH_PACKAGE_ID) + .appendPath(Long.toString(packageId)) + .build(); + } + + @Override + protected String getTableName() { + return PackageTable.NAME; + } + + @Override + protected String getProviderName() { + return "PackageProvider"; + } + + public static String getAuthority() { + return AUTHORITY + "." + PROVIDER_NAME; + } + + @Override + protected UriMatcher getMatcher() { + return MATCHER; + } + + protected QuerySelection querySingle(String packageName) { + final String selection = getTableName() + "." + Cols.PACKAGE_NAME + " = ?"; + final String[] args = {packageName}; + return new QuerySelection(selection, args); + } + + @Override + public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) { + if (MATCHER.match(uri) != CODE_SINGLE) { + throw new UnsupportedOperationException("Invalid URI for content provider: " + uri); + } + + QuerySelection selection = new QuerySelection(customSelection, selectionArgs) + .add(querySingle(uri.getLastPathSegment())); + + Query query = new Query(); + query.addSelection(selection); + query.addFields(projection); + query.addOrderBy(sortOrder); + + Cursor cursor = LoggingQuery.query(db(), query.toString(), query.getArgs()); + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } + + /** + * Deleting of packages is not required. + * It doesn't matter if we have a package name in the database after the package is no longer + * present in the repo any more. They wont take up much space, and it is the presence of rows + * in the {@link Schema.AppMetadataTable} which decides whether something is available in the + * F-Droid client or not. + */ + @Override + public int delete(Uri uri, String where, String[] whereArgs) { + throw new UnsupportedOperationException("Delete not supported for " + uri + "."); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + long rowId = db().insertOrThrow(getTableName(), null, values); + getContext().getContentResolver().notifyChange(AppProvider.getCanUpdateUri(), null); + return getPackageIdUri(rowId); + } + + /** + * Package names never change. If a package name has changed, then that means that it is a + * new app all together as far as Android is concerned. + */ + @Override + public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { + throw new UnsupportedOperationException("Update not supported for " + uri + "."); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java b/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java index 68e2912fc..63e9d76fc 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java +++ b/app/src/main/java/org/fdroid/fdroid/data/RepoPersister.java @@ -143,8 +143,8 @@ public class RepoPersister { for (App app : apps) { packageNames.add(app.packageName); } - String[] projection = {Schema.AppMetadataTable.Cols.ROW_ID, Schema.AppMetadataTable.Cols.PACKAGE_NAME}; - List fromDb = TempAppProvider.Helper.findByPackageNames(context, packageNames, projection); + String[] projection = {Schema.AppMetadataTable.Cols.ROW_ID, Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME}; + List fromDb = TempAppProvider.Helper.findByPackageNames(context, packageNames, repo.id, projection); Map ids = new HashMap<>(fromDb.size()); for (App app : fromDb) { @@ -175,15 +175,17 @@ public class RepoPersister { */ private ArrayList insertOrUpdateApks(List packages) { String[] projection = new String[]{ - Schema.ApkTable.Cols.App.PACKAGE_NAME, + Schema.ApkTable.Cols.Package.PACKAGE_NAME, Schema.ApkTable.Cols.VERSION_CODE, + Schema.ApkTable.Cols.REPO_ID, + Schema.ApkTable.Cols.APP_ID, }; List existingApks = ApkProvider.Helper.knownApks(context, packages, projection); ArrayList operations = new ArrayList<>(packages.size()); for (Apk apk : packages) { boolean exists = false; for (Apk existing : existingApks) { - if (existing.packageName.equals(apk.packageName) && existing.versionCode == apk.versionCode) { + if (existing.repo == apk.repo && existing.packageName.equals(apk.packageName) && existing.versionCode == apk.versionCode) { exists = true; break; } @@ -204,7 +206,7 @@ public class RepoPersister { * Does not do any checks to see if the app already exists or not. */ private ContentProviderOperation updateExistingApp(App app) { - Uri uri = TempAppProvider.getAppUri(app); + Uri uri = TempAppProvider.getSpecificTempAppUri(app.packageName, app.repoId); return ContentProviderOperation.newUpdate(uri).withValues(app.toContentValues()).build(); } @@ -224,8 +226,8 @@ public class RepoPersister { * array. */ private boolean isAppInDatabase(App app) { - String[] fields = {Schema.AppMetadataTable.Cols.PACKAGE_NAME}; - App found = AppProvider.Helper.findByPackageName(context.getContentResolver(), app.packageName, fields); + String[] fields = {Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME}; + App found = AppProvider.Helper.findSpecificApp(context.getContentResolver(), app.packageName, repo.id, fields); return found != null; } @@ -255,8 +257,8 @@ public class RepoPersister { */ @Nullable private ContentProviderOperation deleteOrphanedApks(List apps, Map> packages) { - String[] projection = new String[]{Schema.ApkTable.Cols.App.PACKAGE_NAME, Schema.ApkTable.Cols.VERSION_CODE}; - List existing = ApkProvider.Helper.find(context, repo, apps, projection); + String[] projection = new String[]{Schema.ApkTable.Cols.Package.PACKAGE_NAME, Schema.ApkTable.Cols.VERSION_CODE}; + List existing = ApkProvider.Helper.findByUri(context, repo, apps, projection); List toDelete = new ArrayList<>(); for (Apk existingApk : existing) { diff --git a/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java b/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java index f08fc1589..e5a6b5347 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/RepoProvider.java @@ -232,6 +232,8 @@ public class RepoProvider extends FDroidProvider { Uri appUri = AppProvider.getNoApksUri(); int appCount = resolver.delete(appUri, null, null); Utils.debugLog(TAG, "Removed " + appCount + " apps with no apks."); + + AppProvider.Helper.recalculatePreferredMetadata(context); } public static int countAppsForRepo(Context context, long repoId) { @@ -301,7 +303,7 @@ public class RepoProvider extends FDroidProvider { public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if (TextUtils.isEmpty(sortOrder)) { - sortOrder = "_ID ASC"; + sortOrder = Cols.PRIORITY + " ASC"; } switch (MATCHER.match(uri)) { @@ -399,7 +401,37 @@ public class RepoProvider extends FDroidProvider { @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { + + // When the priority of a repo changes, we need to update the "preferred metadata" foreign + // key in the package table to point to the best possible record in the app metadata table. + // The full list of times when we need to recalculate the preferred metadata includes: + // * After the priority of a repo changes + // * After a repo is disabled + // * After a repo is enabled + // * After an update is performed + // This code only checks for the priority changing. All other occasions we can't do the + // recalculation right now, because we likely haven't added/removed the relevant apps + // from the metadata table yet. Usually the repo details are updated, then a request is + // made to do the heavier work (e.g. a repo update to get new list of apps from server). + // After the heavier work is complete, then that process can request the preferred metadata + // to be recalculated. + boolean priorityChanged = false; + if (values.containsKey(Cols.PRIORITY)) { + Cursor priorityCursor = db().query(getTableName(), new String[]{Cols.PRIORITY}, where, whereArgs, null, null, null); + if (priorityCursor.getCount() > 0) { + priorityCursor.moveToFirst(); + int oldPriority = priorityCursor.getInt(priorityCursor.getColumnIndex(Cols.PRIORITY)); + priorityChanged = oldPriority != values.getAsInteger(Cols.PRIORITY); + } + priorityCursor.close(); + } + int numRows = db().update(getTableName(), values, where, whereArgs); + + if (priorityChanged) { + AppProvider.Helper.recalculatePreferredMetadata(getContext()); + } + Utils.debugLog(TAG, "Updated repo. Notifying provider change: '" + uri + "'."); getContext().getContentResolver().notifyChange(uri, null); return numRows; diff --git a/app/src/main/java/org/fdroid/fdroid/data/Schema.java b/app/src/main/java/org/fdroid/fdroid/data/Schema.java index dc5432958..f14cb1484 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/Schema.java +++ b/app/src/main/java/org/fdroid/fdroid/data/Schema.java @@ -9,6 +9,30 @@ import android.provider.BaseColumns; */ public interface Schema { + interface PackageTable { + + String NAME = "fdroid_package"; + + interface Cols { + String ROW_ID = "rowid"; + String PACKAGE_NAME = "packageName"; + + /** + * Metadata about a package (e.g. description, icon, etc) can come from multiple + * different repos. This is a foreign key to the row in {@link AppMetadataTable} for + * this package that comes from the repo with the best priority. Although it can be + * calculated at runtime using an SQL query, it is more efficient to figure out the + * preferred metadata once, after a repo update, rather than every time we need to know + * about a package. + */ + String PREFERRED_METADATA = "preferredMetadata"; + + String[] ALL = { + ROW_ID, PACKAGE_NAME, PREFERRED_METADATA, + }; + } + } + interface AppPrefsTable { String NAME = "fdroid_appPrefs"; @@ -39,7 +63,8 @@ public interface Schema { String ROW_ID = "rowid"; String _COUNT = "_count"; String IS_COMPATIBLE = "compatible"; - String PACKAGE_NAME = "id"; + String PACKAGE_ID = "packageId"; + String REPO_ID = "repoId"; String NAME = "name"; String SUMMARY = "summary"; String ICON = "icon"; @@ -76,13 +101,17 @@ public interface Schema { String SIGNATURE = "installedSig"; } + interface Package { + String PACKAGE_NAME = "package_packageName"; + } + /** * Each of the physical columns in the sqlite table. Differs from {@link Cols#ALL} in * that it doesn't include fields which are aliases of other fields (e.g. {@link Cols#_ID} * or which are from other related tables (e.g. {@link Cols.SuggestedApk#VERSION_NAME}). */ String[] ALL_COLS = { - ROW_ID, IS_COMPATIBLE, PACKAGE_NAME, NAME, SUMMARY, ICON, DESCRIPTION, + ROW_ID, PACKAGE_ID, REPO_ID, IS_COMPATIBLE, NAME, SUMMARY, ICON, DESCRIPTION, LICENSE, AUTHOR, EMAIL, WEB_URL, TRACKER_URL, SOURCE_URL, CHANGELOG_URL, DONATE_URL, BITCOIN_ADDR, LITECOIN_ADDR, FLATTR_ID, UPSTREAM_VERSION_NAME, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED, @@ -96,14 +125,14 @@ public interface Schema { * @see Cols#ALL_COLS */ String[] ALL = { - _ID, ROW_ID, IS_COMPATIBLE, PACKAGE_NAME, NAME, SUMMARY, ICON, DESCRIPTION, + _ID, ROW_ID, REPO_ID, IS_COMPATIBLE, NAME, SUMMARY, ICON, DESCRIPTION, LICENSE, AUTHOR, EMAIL, WEB_URL, TRACKER_URL, SOURCE_URL, CHANGELOG_URL, DONATE_URL, BITCOIN_ADDR, LITECOIN_ADDR, FLATTR_ID, UPSTREAM_VERSION_NAME, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED, CATEGORIES, ANTI_FEATURES, REQUIREMENTS, ICON_URL, ICON_URL_LARGE, SUGGESTED_VERSION_CODE, SuggestedApk.VERSION_NAME, InstalledApp.VERSION_CODE, InstalledApp.VERSION_NAME, - InstalledApp.SIGNATURE, + InstalledApp.SIGNATURE, Package.PACKAGE_NAME, }; } } @@ -149,8 +178,8 @@ public interface Schema { String ADDRESS = "repoAddress"; } - interface App { - String PACKAGE_NAME = "appPackageName"; + interface Package { + String PACKAGE_NAME = "package_packageName"; } /** @@ -167,7 +196,7 @@ public interface Schema { * @see AppMetadataTable.Cols#ALL */ String[] ALL = { - _ID, APP_ID, App.PACKAGE_NAME, VERSION_NAME, REPO_ID, HASH, VERSION_CODE, NAME, + _ID, APP_ID, Package.PACKAGE_NAME, VERSION_NAME, REPO_ID, HASH, VERSION_CODE, NAME, SIZE, SIGNATURE, SOURCE_NAME, MIN_SDK_VERSION, TARGET_SDK_VERSION, MAX_SDK_VERSION, PERMISSIONS, FEATURES, NATIVE_CODE, HASH_TYPE, ADDED_DATE, IS_COMPATIBLE, Repo.VERSION, Repo.ADDRESS, INCOMPATIBLE_REASONS, 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 8cc83f773..e7465e30b 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/TempApkProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/TempApkProvider.java @@ -27,7 +27,8 @@ public class TempApkProvider extends ApkProvider { static { MATCHER.addURI(getAuthority(), PATH_INIT, CODE_INIT); - MATCHER.addURI(getAuthority(), PATH_APK + "/#/*", CODE_SINGLE); + MATCHER.addURI(getAuthority(), PATH_APK_FROM_ANY_REPO + "/#/*", CODE_APK_FROM_ANY_REPO); + MATCHER.addURI(getAuthority(), PATH_APK_FROM_REPO + "/#/#", CODE_APK_FROM_REPO); MATCHER.addURI(getAuthority(), PATH_REPO_APK + "/#/*", CODE_REPO_APK); } @@ -52,9 +53,9 @@ public class TempApkProvider extends ApkProvider { public static Uri getApkUri(Apk apk) { return getContentUri() .buildUpon() - .appendPath(PATH_APK) + .appendPath(PATH_APK_FROM_REPO) + .appendPath(Long.toString(apk.appId)) .appendPath(Integer.toString(apk.versionCode)) - .appendPath(apk.packageName) .build(); } @@ -96,7 +97,7 @@ public class TempApkProvider extends ApkProvider { @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { - if (MATCHER.match(uri) != CODE_SINGLE) { + if (MATCHER.match(uri) != CODE_APK_FROM_REPO) { throw new UnsupportedOperationException("Cannot update anything other than a single apk."); } 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 955e82cde..6ebc1112f 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/TempAppProvider.java @@ -13,6 +13,8 @@ import java.util.List; import org.fdroid.fdroid.data.Schema.ApkTable; import org.fdroid.fdroid.data.Schema.AppMetadataTable; +import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; +import org.fdroid.fdroid.data.Schema.PackageTable; /** * This class does all of its operations in a temporary sqlite table. @@ -40,8 +42,8 @@ public class TempAppProvider extends AppProvider { static { MATCHER.addURI(getAuthority(), PATH_INIT, CODE_INIT); MATCHER.addURI(getAuthority(), PATH_COMMIT, CODE_COMMIT); - MATCHER.addURI(getAuthority(), PATH_APPS + "/*", APPS); - MATCHER.addURI(getAuthority(), "*", CODE_SINGLE); + MATCHER.addURI(getAuthority(), PATH_APPS + "/#/*", APPS); + MATCHER.addURI(getAuthority(), PATH_SPECIFIC_APP + "/#/*", CODE_SINGLE); } @Override @@ -57,19 +59,36 @@ public class TempAppProvider extends AppProvider { return Uri.parse("content://" + getAuthority()); } - public static Uri getAppUri(App app) { - return Uri.withAppendedPath(getContentUri(), app.packageName); + /** + * Same as {@link AppProvider#getSpecificAppUri(String, long)}, except loads data from the temp + * table being used during a repo update rather than the persistent table. + */ + public static Uri getSpecificTempAppUri(String packageName, long repoId) { + return getContentUri() + .buildUpon() + .appendPath(PATH_SPECIFIC_APP) + .appendPath(Long.toString(repoId)) + .appendPath(packageName) + .build(); } - public static Uri getAppsUri(List apps) { + public static Uri getAppsUri(List apps, long repoId) { return getContentUri().buildUpon() .appendPath(PATH_APPS) + .appendPath(Long.toString(repoId)) .appendPath(TextUtils.join(",", apps)) .build(); } - private AppQuerySelection queryApps(String packageNames) { - return queryPackageNames(packageNames, getTableName() + "." + AppMetadataTable.Cols.PACKAGE_NAME); + private AppQuerySelection queryRepoApps(long repoId, String packageNames) { + return queryPackageNames(packageNames, PackageTable.NAME + "." + PackageTable.Cols.PACKAGE_NAME) + .add(queryRepo(repoId)); + } + + private AppQuerySelection queryRepo(long repoId) { + String[] args = new String[] {Long.toString(repoId)}; + String selection = getTableName() + "." + Cols.REPO_ID + " = ? "; + return new AppQuerySelection(selection, args); } public static class Helper { @@ -84,8 +103,8 @@ public class TempAppProvider extends AppProvider { TempApkProvider.Helper.init(context); } - public static List findByPackageNames(Context context, List packageNames, String[] projection) { - Uri uri = getAppsUri(packageNames); + public static List findByPackageNames(Context context, List packageNames, long repoId, String[] projection) { + Uri uri = getAppsUri(packageNames, repoId); Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); return AppProvider.Helper.cursorToList(cursor); } @@ -126,11 +145,17 @@ public class TempAppProvider extends AppProvider { throw new UnsupportedOperationException("Update not supported for " + uri + "."); } - QuerySelection query = new QuerySelection(where, whereArgs).add(querySingle(uri.getLastPathSegment())); + List pathParts = uri.getPathSegments(); + String packageName = pathParts.get(2); + long repoId = Long.parseLong(pathParts.get(1)); + QuerySelection query = new QuerySelection(where, whereArgs).add(querySingleForUpdate(packageName, repoId)); + + // Package names for apps cannot change... + values.remove(Cols.Package.PACKAGE_NAME); int count = db().update(getTableName(), values, query.getSelection(), query.getArgs()); if (!isApplyingBatch()) { - getContext().getContentResolver().notifyChange(uri, null); + getContext().getContentResolver().notifyChange(getHighestPriorityMetadataUri(packageName), null); } return count; } @@ -140,7 +165,8 @@ public class TempAppProvider extends AppProvider { AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs); switch (MATCHER.match(uri)) { case APPS: - selection = selection.add(queryApps(uri.getLastPathSegment())); + List segments = uri.getPathSegments(); + selection = selection.add(queryRepoApps(Long.parseLong(segments.get(1)), segments.get(2))); break; } @@ -163,7 +189,7 @@ public class TempAppProvider extends AppProvider { db.execSQL("ATTACH DATABASE ':memory:' AS " + DB); db.execSQL(DBHelper.CREATE_TABLE_APP_METADATA.replaceFirst(AppMetadataTable.NAME, DB + "." + getTableName())); db.execSQL(copyData(AppMetadataTable.Cols.ALL_COLS, AppMetadataTable.NAME, DB + "." + getTableName())); - db.execSQL("CREATE INDEX IF NOT EXISTS " + DB + ".app_id ON " + getTableName() + " (" + AppMetadataTable.Cols.PACKAGE_NAME + ");"); + db.execSQL("CREATE INDEX IF NOT EXISTS " + DB + ".app_id ON " + getTableName() + " (" + AppMetadataTable.Cols.PACKAGE_ID + ");"); db.execSQL("CREATE INDEX IF NOT EXISTS " + DB + ".app_upstreamVercode ON " + getTableName() + " (" + AppMetadataTable.Cols.UPSTREAM_VERSION_CODE + ");"); db.execSQL("CREATE INDEX IF NOT EXISTS " + DB + ".app_compatible ON " + getTableName() + " (" + AppMetadataTable.Cols.IS_COMPATIBLE + ");"); } diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 2e3564507..63e25b9e3 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -24,6 +24,7 @@ import org.fdroid.fdroid.compat.PackageManagerCompat; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.DownloaderService; @@ -259,7 +260,7 @@ public class InstallManagerService extends Service { App app = getAppFromActive(downloadUrl); if (app == null) { ContentResolver resolver = context.getContentResolver(); - app = AppProvider.Helper.findByPackageName(resolver, apk.packageName); + app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo); } // show notification if app details is not visible if (app != null && AppDetails.isAppVisible(app.packageName)) { @@ -346,7 +347,8 @@ public class InstallManagerService extends Service { String name = getAppName(apk); if (TextUtils.isEmpty(name) || name.equals(new App().name)) { ContentResolver resolver = getContentResolver(); - App app = AppProvider.Helper.findByPackageName(resolver, apk.packageName); + App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo, + new String[] {Schema.AppMetadataTable.Cols.NAME}); if (app == null || TextUtils.isEmpty(app.name)) { return; // do not have a name to display, so leave notification as is } diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java index a14996357..230efd70d 100644 --- a/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/InstallConfirmActivity.java @@ -191,7 +191,7 @@ public class InstallConfirmActivity extends FragmentActivity implements OnCancel intent = getIntent(); Uri uri = intent.getData(); Apk apk = ApkProvider.Helper.findByUri(this, uri, Schema.ApkTable.Cols.ALL); - app = AppProvider.Helper.findByPackageName(getContentResolver(), apk.packageName); + app = AppProvider.Helper.findSpecificApp(getContentResolver(), apk.packageName, apk.repo, Schema.AppMetadataTable.Cols.ALL); appDiff = new AppDiff(getPackageManager(), apk); diff --git a/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java b/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java index 7ffa038b5..188861f01 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java +++ b/app/src/main/java/org/fdroid/fdroid/views/fragments/AppListFragment.java @@ -39,7 +39,7 @@ public abstract class AppListFragment extends ListFragment implements private static final String[] APP_PROJECTION = { AppMetadataTable.Cols._ID, // Required for cursor loader to work. - AppMetadataTable.Cols.PACKAGE_NAME, + AppMetadataTable.Cols.Package.PACKAGE_NAME, AppMetadataTable.Cols.NAME, AppMetadataTable.Cols.SUMMARY, AppMetadataTable.Cols.IS_COMPATIBLE, diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapAppsView.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapAppsView.java index 0b570506c..a252b76a2 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapAppsView.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapAppsView.java @@ -289,7 +289,8 @@ public class SwapAppsView extends ListView implements public void onChange(boolean selfChange) { Activity activity = getActivity(); if (activity != null) { - app = AppProvider.Helper.findByPackageName(getActivity().getContentResolver(), app.packageName); + app = AppProvider.Helper.findSpecificApp(getActivity().getContentResolver(), + app.packageName, app.repoId, AppMetadataTable.Cols.ALL); resetView(); } } @@ -318,7 +319,7 @@ public class SwapAppsView extends ListView implements // implemented on API-16, so leaving like this for now. getActivity().getContentResolver().unregisterContentObserver(appObserver); getActivity().getContentResolver().registerContentObserver( - AppProvider.getContentUri(this.app.packageName), true, appObserver); + AppProvider.getSpecificAppUri(this.app.packageName, this.app.repoId), true, appObserver); } resetView(); } diff --git a/app/src/main/res/values/default_repos.xml b/app/src/main/res/values/default_repos.xml index 6a09a6920..3a895f579 100644 --- a/app/src/main/res/values/default_repos.xml +++ b/app/src/main/res/values/default_repos.xml @@ -17,7 +17,7 @@ 1 - 10 + 1 ignore @@ -38,7 +38,7 @@ 0 - 20 + 2 ignore @@ -60,7 +60,7 @@ 0 - 10 + 3 ignore @@ -81,7 +81,7 @@ 0 - 20 + 4 ignore diff --git a/app/src/test/java/org/fdroid/fdroid/Assert.java b/app/src/test/java/org/fdroid/fdroid/Assert.java index 61f6fe223..27036a5f3 100644 --- a/app/src/test/java/org/fdroid/fdroid/Assert.java +++ b/app/src/test/java/org/fdroid/fdroid/Assert.java @@ -183,7 +183,8 @@ public class Assert { public static App insertApp(Context context, String packageName, String name, ContentValues additionalValues) { ContentValues values = new ContentValues(); - values.put(AppMetadataTable.Cols.PACKAGE_NAME, packageName); + values.put(AppMetadataTable.Cols.REPO_ID, 1); + values.put(AppMetadataTable.Cols.Package.PACKAGE_NAME, packageName); values.put(AppMetadataTable.Cols.NAME, name); // Required fields (NOT NULL in the database). @@ -197,14 +198,14 @@ public class Assert { Uri uri = AppProvider.getContentUri(); context.getContentResolver().insert(uri, values); - return AppProvider.Helper.findByPackageName(context.getContentResolver(), packageName); + return AppProvider.Helper.findSpecificApp(context.getContentResolver(), packageName, 1, AppMetadataTable.Cols.ALL); } - private static App ensureApp(Context context, String packageName) { - App app = AppProvider.Helper.findByPackageName(context.getContentResolver(), packageName); + public static App ensureApp(Context context, String packageName) { + App app = AppProvider.Helper.findSpecificApp(context.getContentResolver(), packageName, 1, AppMetadataTable.Cols.ALL); if (app == null) { insertApp(context, packageName, packageName); - app = AppProvider.Helper.findByPackageName(context.getContentResolver(), packageName); + app = AppProvider.Helper.findSpecificApp(context.getContentResolver(), packageName, 1, AppMetadataTable.Cols.ALL); } assertNotNull(app); return app; diff --git a/app/src/test/java/org/fdroid/fdroid/ProperMultiRepoUpdaterTest.java b/app/src/test/java/org/fdroid/fdroid/ProperMultiRepoUpdaterTest.java deleted file mode 100644 index 3d6e18abd..000000000 --- a/app/src/test/java/org/fdroid/fdroid/ProperMultiRepoUpdaterTest.java +++ /dev/null @@ -1,155 +0,0 @@ - -package org.fdroid.fdroid; - -import android.util.Log; - -import org.fdroid.fdroid.data.Apk; -import org.fdroid.fdroid.data.ApkProvider; -import org.fdroid.fdroid.data.Repo; -import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.data.Schema; - -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/* -At time fo writing, the following tests did not pass. This is because the multi-repo support -in F-Droid was not sufficient. When working on proper multi repo support than this should be -uncommented and all these tests will be required to pass: - -@Config(constants = BuildConfig.class) -@RunWith(RobolectricGradleTestRunner.class) -*/ -@SuppressWarnings("unused") -public class ProperMultiRepoUpdaterTest extends MultiRepoUpdaterTest { - private static final String TAG = "ProperMultiRepoSupport"; - - /*@Test - public void testCorrectConflictingThenMainThenArchive() throws UpdateException { - assertEmpty(); - if (updateConflicting() && updateMain() && updateArchive()) { - assertExpected(); - } - } - - @Test - public void testCorrectConflictingThenArchiveThenMain() throws UpdateException { - assertEmpty(); - if (updateConflicting() && updateArchive() && updateMain()) { - assertExpected(); - } - } - - @Test - public void testCorrectArchiveThenMainThenConflicting() throws UpdateException { - assertEmpty(); - if (updateArchive() && updateMain() && updateConflicting()) { - assertExpected(); - } - } - - @Test - public void testCorrectArchiveThenConflictingThenMain() throws UpdateException { - assertEmpty(); - if (updateArchive() && updateConflicting() && updateMain()) { - assertExpected(); - } - } - - @Test - public void testCorrectMainThenArchiveThenConflicting() throws UpdateException { - assertEmpty(); - if (updateMain() && updateArchive() && updateConflicting()) { - assertExpected(); - } - } - - @Test - public void testCorrectMainThenConflictingThenArchive() throws UpdateException { - assertEmpty(); - if (updateMain() && updateConflicting() && updateArchive()) { - assertExpected(); - } - }*/ - - /** - * Check that all of the expected apps and apk versions are available in the database. This - * check will take into account the repository the apks came from, to ensure that each - * repository indeed contains the apks that it said it would provide. - */ - private void assertExpected() { - Log.i(TAG, "Asserting all versions of each .apk are in index."); - List repos = RepoProvider.Helper.all(context); - assertEquals("Repos", 3, repos.size()); - - assertMainRepo(repos); - assertMainArchiveRepo(repos); - assertConflictingRepo(repos); - } - - /** - * + 2048 (com.uberspot.a2048) - * - Version 1.96 (19) - * - Version 1.95 (18) - * + AdAway (org.adaway) - * - Version 3.0.2 (54) - * - Version 3.0.1 (53) - * - Version 3.0 (52) - * + adbWireless (siir.es.adbWireless) - * - Version 1.5.4 (12) - */ - private void assertMainRepo(List allRepos) { - Repo repo = findRepo(REPO_MAIN, allRepos); - - List apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL); - assertEquals("Apks for main repo", apks.size(), 6); - assertApksExist(apks, "com.uberspot.a2048", new int[]{18, 19}); - assertApksExist(apks, "org.adaway", new int[]{52, 53, 54}); - assertApksExist(apks, "siir.es.adbWireless", new int[]{12}); - } - - /** - * + AdAway (org.adaway) - * - Version 2.9.2 (51) - * - Version 2.9.1 (50) - * - Version 2.9 (49) - * - Version 2.8.1 (48) - * - Version 2.8 (47) - * - Version 2.7 (46) - * - Version 2.6 (45) - * - Version 2.3 (42) - * - Version 2.1 (40) - * - Version 1.37 (38) - * - Version 1.36 (37) - * - Version 1.35 (36) - * - Version 1.34 (35) - */ - private void assertMainArchiveRepo(List allRepos) { - Repo repo = findRepo(REPO_ARCHIVE, allRepos); - - List apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL); - assertEquals("Apks for main archive repo", 13, apks.size()); - assertApksExist(apks, "org.adaway", new int[]{35, 36, 37, 38, 40, 42, 45, 46, 47, 48, 49, 50, 51}); - } - - /** - * + AdAway (org.adaway) - * - Version 3.0.1 (53) * - * - Version 3.0 (52) * - * - Version 2.9.2 (51) * - * - Version 2.2.1 (50) * - * + Add to calendar (org.dgtale.icsimport) - * - Version 1.2 (3) - * - Version 1.1 (2) - */ - private void assertConflictingRepo(List allRepos) { - Repo repo = findRepo(REPO_CONFLICTING, allRepos); - - List apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL); - assertEquals("Apks for main repo", 6, apks.size()); - assertApksExist(apks, "org.adaway", new int[]{50, 51, 52, 53}); - assertApksExist(apks, "org.dgtale.icsimport", new int[]{2, 3}); - } - -} diff --git a/app/src/test/java/org/fdroid/fdroid/data/ApkProviderTest.java b/app/src/test/java/org/fdroid/fdroid/data/ApkProviderTest.java index 95a153071..0f21dd119 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/ApkProviderTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/ApkProviderTest.java @@ -242,41 +242,45 @@ public class ApkProviderTest extends FDroidProviderTest { @Test public void testKnownApks() { + App fdroid = Assert.ensureApp(context, "org.fdroid.fdroid"); for (int i = 0; i < 7; i++) { - Assert.insertApk(context, "org.fdroid.fdroid", i); + Assert.insertApk(context, fdroid, i); } + App exampleOrg = Assert.ensureApp(context, "org.example"); for (int i = 0; i < 9; i++) { - Assert.insertApk(context, "org.example", i); + Assert.insertApk(context, exampleOrg, i); } + App exampleCom = Assert.ensureApp(context, "com.example"); for (int i = 0; i < 3; i++) { - Assert.insertApk(context, "com.example", i); + Assert.insertApk(context, exampleCom, i); } - Assert.insertApk(context, "com.apk.thingo", 1); + App thingo = Assert.ensureApp(context, "com.apk.thingo"); + Assert.insertApk(context, thingo, 1); Apk[] known = { - new MockApk("org.fdroid.fdroid", 1), - new MockApk("org.fdroid.fdroid", 3), - new MockApk("org.fdroid.fdroid", 5), + new MockApk(fdroid, 1), + new MockApk(fdroid, 3), + new MockApk(fdroid, 5), - new MockApk("com.example", 1), - new MockApk("com.example", 2), + new MockApk(exampleCom, 1), + new MockApk(exampleCom, 2), }; Apk[] unknown = { - new MockApk("org.fdroid.fdroid", 7), - new MockApk("org.fdroid.fdroid", 9), - new MockApk("org.fdroid.fdroid", 11), - new MockApk("org.fdroid.fdroid", 13), + new MockApk(fdroid, 7), + new MockApk(fdroid, 9), + new MockApk(fdroid, 11), + new MockApk(fdroid, 13), - new MockApk("com.example", 3), - new MockApk("com.example", 4), - new MockApk("com.example", 5), + new MockApk(exampleCom, 3), + new MockApk(exampleCom, 4), + new MockApk(exampleCom, 5), - new MockApk("info.example", 1), - new MockApk("info.example", 2), + new MockApk(-10, 1), + new MockApk(-10, 2), }; List apksToCheck = new ArrayList<>(known.length + unknown.length); @@ -284,7 +288,8 @@ public class ApkProviderTest extends FDroidProviderTest { Collections.addAll(apksToCheck, unknown); String[] projection = { - Cols.App.PACKAGE_NAME, + Cols.Package.PACKAGE_NAME, + Cols.APP_ID, Cols.VERSION_CODE, }; @@ -424,7 +429,7 @@ public class ApkProviderTest extends FDroidProviderTest { assertEquals("a hash type", apk.hashType); String[] projection = { - Cols.App.PACKAGE_NAME, + Cols.Package.PACKAGE_NAME, Cols.HASH, }; 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 f6dd4995a..0d8102b8d 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/AppProviderTest.java @@ -68,7 +68,7 @@ public class AppProviderTest extends FDroidProviderTest { @Test public void testCantFindApp() { - assertNull(AppProvider.Helper.findByPackageName(context.getContentResolver(), "com.example.doesnt-exist")); + assertNull(AppProvider.Helper.findSpecificApp(context.getContentResolver(), "com.example.doesnt-exist", 1, Cols.ALL)); } @Test @@ -111,14 +111,14 @@ public class AppProviderTest extends FDroidProviderTest { ContentResolver r = context.getContentResolver(); // Can't "update", although can "install"... - App notInstalled = AppProvider.Helper.findByPackageName(r, "not installed"); + App notInstalled = AppProvider.Helper.findSpecificApp(r, "not installed", 1, Cols.ALL); assertFalse(notInstalled.canAndWantToUpdate(context)); - App installedOnlyOneVersionAvailable = AppProvider.Helper.findByPackageName(r, "installed, only one version available"); - App installedAlreadyLatestNoIgnore = AppProvider.Helper.findByPackageName(r, "installed, already latest, no ignore"); - App installedAlreadyLatestIgnoreAll = AppProvider.Helper.findByPackageName(r, "installed, already latest, ignore all"); - App installedAlreadyLatestIgnoreLatest = AppProvider.Helper.findByPackageName(r, "installed, already latest, ignore latest"); - App installedAlreadyLatestIgnoreOld = AppProvider.Helper.findByPackageName(r, "installed, already latest, ignore old"); + App installedOnlyOneVersionAvailable = AppProvider.Helper.findSpecificApp(r, "installed, only one version available", 1, Cols.ALL); + App installedAlreadyLatestNoIgnore = AppProvider.Helper.findSpecificApp(r, "installed, already latest, no ignore", 1, Cols.ALL); + App installedAlreadyLatestIgnoreAll = AppProvider.Helper.findSpecificApp(r, "installed, already latest, ignore all", 1, Cols.ALL); + App installedAlreadyLatestIgnoreLatest = AppProvider.Helper.findSpecificApp(r, "installed, already latest, ignore latest", 1, Cols.ALL); + App installedAlreadyLatestIgnoreOld = AppProvider.Helper.findSpecificApp(r, "installed, already latest, ignore old", 1, Cols.ALL); assertFalse(installedOnlyOneVersionAvailable.canAndWantToUpdate(context)); assertFalse(installedAlreadyLatestNoIgnore.canAndWantToUpdate(context)); @@ -126,10 +126,10 @@ public class AppProviderTest extends FDroidProviderTest { assertFalse(installedAlreadyLatestIgnoreLatest.canAndWantToUpdate(context)); assertFalse(installedAlreadyLatestIgnoreOld.canAndWantToUpdate(context)); - App installedOldNoIgnore = AppProvider.Helper.findByPackageName(r, "installed, old version, no ignore"); - App installedOldIgnoreAll = AppProvider.Helper.findByPackageName(r, "installed, old version, ignore all"); - App installedOldIgnoreLatest = AppProvider.Helper.findByPackageName(r, "installed, old version, ignore latest"); - App installedOldIgnoreNewerNotLatest = AppProvider.Helper.findByPackageName(r, "installed, old version, ignore newer, but not latest"); + App installedOldNoIgnore = AppProvider.Helper.findSpecificApp(r, "installed, old version, no ignore", 1, Cols.ALL); + App installedOldIgnoreAll = AppProvider.Helper.findSpecificApp(r, "installed, old version, ignore all", 1, Cols.ALL); + App installedOldIgnoreLatest = AppProvider.Helper.findSpecificApp(r, "installed, old version, ignore latest", 1, Cols.ALL); + App installedOldIgnoreNewerNotLatest = AppProvider.Helper.findSpecificApp(r, "installed, old version, ignore newer, but not latest", 1, Cols.ALL); assertTrue(installedOldNoIgnore.canAndWantToUpdate(context)); assertFalse(installedOldIgnoreAll.canAndWantToUpdate(context)); @@ -169,7 +169,7 @@ public class AppProviderTest extends FDroidProviderTest { assertResultCount(contentResolver, 10, AppProvider.getContentUri(), PROJ); - String[] projection = {Cols.PACKAGE_NAME}; + String[] projection = {Cols.Package.PACKAGE_NAME}; List canUpdateApps = AppProvider.Helper.findCanUpdate(context, projection); String[] expectedCanUpdate = { @@ -239,7 +239,7 @@ public class AppProviderTest extends FDroidProviderTest { assertEquals("org.fdroid.fdroid", app.packageName); assertEquals("F-Droid", app.name); - App otherApp = AppProvider.Helper.findByPackageName(context.getContentResolver(), "org.fdroid.fdroid"); + App otherApp = AppProvider.Helper.findSpecificApp(context.getContentResolver(), "org.fdroid.fdroid", 1, Cols.ALL); assertNotNull(otherApp); assertEquals("org.fdroid.fdroid", otherApp.packageName); assertEquals("F-Droid", otherApp.name); @@ -260,7 +260,7 @@ public class AppProviderTest extends FDroidProviderTest { String[] projection = new String[] { Cols._ID, Cols.NAME, - Cols.PACKAGE_NAME, + Cols.Package.PACKAGE_NAME, }; return contentResolver.query(AppProvider.getContentUri(), projection, null, null, null); } @@ -356,7 +356,8 @@ public class AppProviderTest extends FDroidProviderTest { public App insertApp(String id, String name, ContentValues additionalValues) { ContentValues values = new ContentValues(); - values.put(Cols.PACKAGE_NAME, id); + values.put(Cols.Package.PACKAGE_NAME, id); + values.put(Cols.REPO_ID, 1); values.put(Cols.NAME, name); // Required fields (NOT NULL in the database). @@ -370,6 +371,9 @@ public class AppProviderTest extends FDroidProviderTest { Uri uri = AppProvider.getContentUri(); contentResolver.insert(uri, values); - return AppProvider.Helper.findByPackageName(context.getContentResolver(), id); + + AppProvider.Helper.recalculatePreferredMetadata(context); + + return AppProvider.Helper.findSpecificApp(context.getContentResolver(), id, 1, Cols.ALL); } } diff --git a/app/src/test/java/org/fdroid/fdroid/data/ProviderUriTests.java b/app/src/test/java/org/fdroid/fdroid/data/ProviderUriTests.java index 2c5f067e0..788c3aa31 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/ProviderUriTests.java +++ b/app/src/test/java/org/fdroid/fdroid/data/ProviderUriTests.java @@ -93,10 +93,10 @@ public class ProviderUriTests { assertValidUri(resolver, AppProvider.getCanUpdateUri(), "content://org.fdroid.fdroid.data.AppProvider/canUpdate", projection); App app = new App(); + app.repoId = 1; app.packageName = "org.fdroid.fdroid"; - assertValidUri(resolver, AppProvider.getContentUri(app), "content://org.fdroid.fdroid.data.AppProvider/org.fdroid.fdroid", projection); - assertValidUri(resolver, AppProvider.getContentUri("org.fdroid.fdroid"), "content://org.fdroid.fdroid.data.AppProvider/org.fdroid.fdroid", projection); + assertValidUri(resolver, AppProvider.getSpecificAppUri(app.packageName, app.repoId), "content://org.fdroid.fdroid.data.AppProvider/app/1/org.fdroid.fdroid", projection); } @Test @@ -112,7 +112,7 @@ public class ProviderUriTests { packageNames.add("org.fdroid.fdroid"); packageNames.add("com.example.com"); - assertValidUri(resolver, TempAppProvider.getAppsUri(packageNames), "content://org.fdroid.fdroid.data.TempAppProvider/apps/org.fdroid.fdroid%2Ccom.example.com", projection); + assertValidUri(resolver, TempAppProvider.getAppsUri(packageNames, 1), "content://org.fdroid.fdroid.data.TempAppProvider/apps/1/org.fdroid.fdroid%2Ccom.example.com", projection); assertValidUri(resolver, TempAppProvider.getContentUri(), "content://org.fdroid.fdroid.data.TempAppProvider", projection); } @@ -135,9 +135,9 @@ public class ProviderUriTests { assertValidUri(resolver, ApkProvider.getContentUri(), "content://org.fdroid.fdroid.data.ApkProvider", projection); assertValidUri(resolver, ApkProvider.getAppUri("org.fdroid.fdroid"), "content://org.fdroid.fdroid.data.ApkProvider/app/org.fdroid.fdroid", projection); - assertValidUri(resolver, ApkProvider.getApkFromAnyRepoUri(new MockApk("org.fdroid.fdroid", 100)), "content://org.fdroid.fdroid.data.ApkProvider/apk/100/org.fdroid.fdroid", projection); + assertValidUri(resolver, ApkProvider.getApkFromAnyRepoUri(new MockApk("org.fdroid.fdroid", 100)), "content://org.fdroid.fdroid.data.ApkProvider/apk-any-repo/100/org.fdroid.fdroid", projection); assertValidUri(resolver, ApkProvider.getContentUri(apks), projection); - assertValidUri(resolver, ApkProvider.getApkFromAnyRepoUri("org.fdroid.fdroid", 100), "content://org.fdroid.fdroid.data.ApkProvider/apk/100/org.fdroid.fdroid", projection); + assertValidUri(resolver, ApkProvider.getApkFromAnyRepoUri("org.fdroid.fdroid", 100), "content://org.fdroid.fdroid.data.ApkProvider/apk-any-repo/100/org.fdroid.fdroid", projection); assertValidUri(resolver, ApkProvider.getRepoUri(1000), "content://org.fdroid.fdroid.data.ApkProvider/repo/1000", projection); } diff --git a/app/src/test/java/org/fdroid/fdroid/mock/MockApk.java b/app/src/test/java/org/fdroid/fdroid/mock/MockApk.java index be7dff5a5..35592526e 100644 --- a/app/src/test/java/org/fdroid/fdroid/mock/MockApk.java +++ b/app/src/test/java/org/fdroid/fdroid/mock/MockApk.java @@ -1,6 +1,7 @@ package org.fdroid.fdroid.mock; import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.App; public class MockApk extends Apk { @@ -9,4 +10,14 @@ public class MockApk extends Apk { this.versionCode = versionCode; } + public MockApk(App app, int versionCode) { + this.appId = app.getId(); + this.versionCode = versionCode; + } + + public MockApk(long appId, int versionCode) { + this.appId = appId; + this.versionCode = versionCode; + } + } diff --git a/app/src/test/java/org/fdroid/fdroid/AcceptableMultiRepoUpdaterTest.java b/app/src/test/java/org/fdroid/fdroid/updater/AcceptableMultiRepoUpdaterTest.java similarity index 79% rename from app/src/test/java/org/fdroid/fdroid/AcceptableMultiRepoUpdaterTest.java rename to app/src/test/java/org/fdroid/fdroid/updater/AcceptableMultiRepoUpdaterTest.java index d0f7b9f13..90ced14d3 100644 --- a/app/src/test/java/org/fdroid/fdroid/AcceptableMultiRepoUpdaterTest.java +++ b/app/src/test/java/org/fdroid/fdroid/updater/AcceptableMultiRepoUpdaterTest.java @@ -1,14 +1,15 @@ -package org.fdroid.fdroid; +package org.fdroid.fdroid.updater; import android.content.ContentValues; import android.support.annotation.NonNull; import android.util.Log; +import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.RepoUpdater.UpdateException; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; -import org.fdroid.fdroid.data.Schema; +import org.fdroid.fdroid.data.Schema.RepoTable.Cols; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricGradleTestRunner; @@ -39,49 +40,67 @@ public class AcceptableMultiRepoUpdaterTest extends MultiRepoUpdaterTest { @Test public void testAcceptableConflictingThenMainThenArchive() throws UpdateException { assertEmpty(); - if (updateConflicting() && updateMain() && updateArchive()) { - assertSomewhatAcceptable(); - } + + updateConflicting(); + updateMain(); + updateArchive(); + + assertSomewhatAcceptable(); } @Test public void testAcceptableConflictingThenArchiveThenMain() throws UpdateException { assertEmpty(); - if (updateConflicting() && updateArchive() && updateMain()) { - assertSomewhatAcceptable(); - } + + updateConflicting(); + updateArchive(); + updateMain(); + + assertSomewhatAcceptable(); } @Test public void testAcceptableArchiveThenMainThenConflicting() throws UpdateException { assertEmpty(); - if (updateArchive() && updateMain() && updateConflicting()) { - assertSomewhatAcceptable(); - } + + updateArchive(); + updateMain(); + updateConflicting(); + + assertSomewhatAcceptable(); } @Test public void testAcceptableArchiveThenConflictingThenMain() throws UpdateException { assertEmpty(); - if (updateArchive() && updateConflicting() && updateMain()) { - assertSomewhatAcceptable(); - } + + updateArchive(); + updateConflicting(); + updateMain(); + + assertSomewhatAcceptable(); } @Test public void testAcceptableMainThenArchiveThenConflicting() throws UpdateException { assertEmpty(); - if (updateMain() && updateArchive() && updateConflicting()) { - assertSomewhatAcceptable(); - } + + updateMain(); + updateArchive(); + updateConflicting(); + + assertSomewhatAcceptable(); } @Test public void testAcceptableMainThenConflictingThenArchive() throws UpdateException { assertEmpty(); - if (updateMain() && updateConflicting() && updateArchive()) { - assertSomewhatAcceptable(); - } + + updateMain(); + updateConflicting(); + updateArchive(); + + assertSomewhatAcceptable(); } @NonNull @@ -128,7 +147,7 @@ public class AcceptableMultiRepoUpdaterTest extends MultiRepoUpdaterTest { private void disableRepo(Repo repo) { ContentValues values = new ContentValues(1); - values.put(Schema.RepoTable.Cols.IN_USE, 0); + values.put(Cols.IN_USE, 0); RepoProvider.Helper.update(context, repo, values); } diff --git a/app/src/test/java/org/fdroid/fdroid/Issue763MultiRepo.java b/app/src/test/java/org/fdroid/fdroid/updater/Issue763MultiRepo.java similarity index 98% rename from app/src/test/java/org/fdroid/fdroid/Issue763MultiRepo.java rename to app/src/test/java/org/fdroid/fdroid/updater/Issue763MultiRepo.java index 8df75aedb..f4f169092 100644 --- a/app/src/test/java/org/fdroid/fdroid/Issue763MultiRepo.java +++ b/app/src/test/java/org/fdroid/fdroid/updater/Issue763MultiRepo.java @@ -1,7 +1,9 @@ -package org.fdroid.fdroid; +package org.fdroid.fdroid.updater; import android.content.ContentValues; +import org.fdroid.fdroid.BuildConfig; +import org.fdroid.fdroid.RepoUpdater; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.Repo; diff --git a/app/src/test/java/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/app/src/test/java/org/fdroid/fdroid/updater/MultiRepoUpdaterTest.java similarity index 91% rename from app/src/test/java/org/fdroid/fdroid/MultiRepoUpdaterTest.java rename to app/src/test/java/org/fdroid/fdroid/updater/MultiRepoUpdaterTest.java index f4c529059..6b761c15b 100644 --- a/app/src/test/java/org/fdroid/fdroid/MultiRepoUpdaterTest.java +++ b/app/src/test/java/org/fdroid/fdroid/updater/MultiRepoUpdaterTest.java @@ -1,12 +1,15 @@ -package org.fdroid.fdroid; +package org.fdroid.fdroid.updater; import android.content.ContentValues; import android.content.Context; import android.support.annotation.NonNull; import android.text.TextUtils; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.RepoUpdater; import org.fdroid.fdroid.RepoUpdater.UpdateException; +import org.fdroid.fdroid.TestUtils; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.AppProvider; @@ -162,7 +165,7 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest { repo.address = uri; repo.name = name; - ContentValues values = new ContentValues(2); + ContentValues values = new ContentValues(3); values.put(Schema.RepoTable.Cols.SIGNING_CERT, repo.signingCertificate); values.put(Schema.RepoTable.Cols.ADDRESS, repo.address); values.put(Schema.RepoTable.Cols.NAME, repo.name); @@ -178,19 +181,19 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest { return new RepoUpdater(context, createRepo(name, uri, context)); } - protected boolean updateConflicting() throws UpdateException { - return updateRepo(createUpdater(REPO_CONFLICTING, REPO_CONFLICTING_URI, context), "multiRepo.conflicting.jar"); + protected void updateConflicting() throws UpdateException { + updateRepo(createUpdater(REPO_CONFLICTING, REPO_CONFLICTING_URI, context), "multiRepo.conflicting.jar"); } - protected boolean updateMain() throws UpdateException { - return updateRepo(createUpdater(REPO_MAIN, REPO_MAIN_URI, context), "multiRepo.normal.jar"); + protected void updateMain() throws UpdateException { + updateRepo(createUpdater(REPO_MAIN, REPO_MAIN_URI, context), "multiRepo.normal.jar"); } - protected boolean updateArchive() throws UpdateException { - return updateRepo(createUpdater(REPO_ARCHIVE, REPO_ARCHIVE_URI, context), "multiRepo.archive.jar"); + protected void updateArchive() throws UpdateException { + updateRepo(createUpdater(REPO_ARCHIVE, REPO_ARCHIVE_URI, context), "multiRepo.archive.jar"); } - protected boolean updateRepo(RepoUpdater updater, String indexJarPath) throws UpdateException { + protected void updateRepo(RepoUpdater updater, String indexJarPath) throws UpdateException { File indexJar = TestUtils.copyResourceToTempFile(indexJarPath); try { updater.processDownloadedFile(indexJar); @@ -199,7 +202,6 @@ public abstract class MultiRepoUpdaterTest extends FDroidProviderTest { indexJar.delete(); } } - return true; } } diff --git a/app/src/test/java/org/fdroid/fdroid/updater/ProperMultiRepoUpdaterTest.java b/app/src/test/java/org/fdroid/fdroid/updater/ProperMultiRepoUpdaterTest.java new file mode 100644 index 000000000..19dea49bb --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/updater/ProperMultiRepoUpdaterTest.java @@ -0,0 +1,390 @@ + +package org.fdroid.fdroid.updater; + +import android.content.ContentValues; +import android.support.annotation.StringDef; +import android.util.Log; + +import org.fdroid.fdroid.BuildConfig; +import org.fdroid.fdroid.RepoUpdater; +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.ApkProvider; +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.Schema; +import org.fdroid.fdroid.data.Schema.AppMetadataTable; +import org.fdroid.fdroid.data.Schema.RepoTable.Cols; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.annotation.Config; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +// TODO: Use sdk=24 when Robolectric supports this +@Config(constants = BuildConfig.class, sdk = 23) +@RunWith(RobolectricGradleTestRunner.class) +public class ProperMultiRepoUpdaterTest extends MultiRepoUpdaterTest { + private static final String TAG = "ProperMultiRepoSupport"; + + @Retention(RetentionPolicy.SOURCE) + @StringDef({"Conflicting", "Normal"}) + public @interface RepoIdentifier { } + + /* + *This test fails due to issue #568 (https://gitlab.com/fdroid/fdroidclient/issues/568). + @Test + public void appsRemovedFromRepo() throws RepoUpdater.UpdateException { + assertEquals(0, AppProvider.Helper.all(context.getContentResolver()).size()); + + updateMain(); + Repo repo = RepoProvider.Helper.findByAddress(context, REPO_MAIN_URI); + + assertEquals(3, AppProvider.Helper.all(context.getContentResolver()).size()); + assertEquals(6, ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL).size()); + assertEquals(3, ApkProvider.Helper.findByPackageName(context, "org.adaway").size()); + assertEquals(2, ApkProvider.Helper.findByPackageName(context, "com.uberspot.a2048").size()); + assertEquals(1, ApkProvider.Helper.findByPackageName(context, "siir.es.adbWireless").size()); + + RepoUpdater updater = new RepoUpdater(context, RepoProvider.Helper.findByAddress(context, repo.address)); + updateRepo(updater, "multiRepo.conflicting.jar"); + + assertEquals(2, AppProvider.Helper.all(context.getContentResolver()).size()); + assertEquals(6, ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL).size()); + assertEquals(4, ApkProvider.Helper.findByPackageName(context, "org.adaway").size()); + assertEquals(2, ApkProvider.Helper.findByPackageName(context, "org.dgtale.icsimport").size()); + }*/ + + @Test + public void mainRepo() throws RepoUpdater.UpdateException { + assertEmpty(); + updateMain(); + assertMainRepo(); + } + + @Test + public void archiveRepo() throws RepoUpdater.UpdateException { + assertEmpty(); + updateArchive(); + assertMainArchiveRepoMetadata(); + } + + @Test + public void conflictingRepo() throws RepoUpdater.UpdateException { + assertEmpty(); + updateConflicting(); + assertConflictingRepo(); + } + + private Map allApps() { + List apps = AppProvider.Helper.all(context.getContentResolver()); + Map appsIndexedByPackageName = new HashMap<>(apps.size()); + for (App app : apps) { + appsIndexedByPackageName.put(app.packageName, app); + } + return appsIndexedByPackageName; + } + + @Test + public void metadataWithRepoPriority() throws RepoUpdater.UpdateException { + updateConflicting(); + updateMain(); + updateArchive(); + + Repo conflictingRepo = RepoProvider.Helper.findByAddress(context, REPO_CONFLICTING_URI); + + assertEquals(1, conflictingRepo.priority); + assertEquals(2, RepoProvider.Helper.findByAddress(context, REPO_MAIN_URI).priority); + assertEquals(3, RepoProvider.Helper.findByAddress(context, REPO_ARCHIVE_URI).priority); + + assertMainRepo(); + assertMainArchiveRepoMetadata(); + assertConflictingRepo(); + + assertRepoTakesPriority("Conflicting"); + + // Make the conflicting repo less important than the main repo. + ContentValues values = new ContentValues(1); + values.put(Cols.PRIORITY, 5); + RepoProvider.Helper.update(context, conflictingRepo, values); + Repo updatedConflictingRepo = RepoProvider.Helper.findByAddress(context, REPO_CONFLICTING_URI); + assertEquals(5, updatedConflictingRepo.priority); + + assertRepoTakesPriority("Normal"); + } + + private void assertRepoTakesPriority(@RepoIdentifier String higherPriority) { + Map allApps = allApps(); + + // Provided by both the "Main" and "Conflicting" repo, so need to fetch metdata from the + // repo with the higher "Conflicting" repo has a higher priority. + App adAway = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), "org.adaway"); + assertAdAwayMetadata(adAway, higherPriority); + assertAdAwayMetadata(allApps.get("org.adaway"), higherPriority); + + + // This is only provided by the "Main" or "Archive" repo. Both the main and archive repo both + // pull their metadata from the same build recipe in fdroidserver. The only difference is that + // the archive repository contains .apks from further back, but their metadata is the same. + App a2048 = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), "com.uberspot.a2048"); + assert2048Metadata(a2048, "Normal"); + assert2048Metadata(allApps.get("com.uberspot.a2048"), "Normal"); + + // This is only provided by the "Conflicting" repo. + App calendar = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), "org.dgtale.icsimport"); + assertCalendarMetadata(calendar, "Conflicting"); + assertCalendarMetadata(allApps.get("org.dgtale.icsimport"), "Conflicting"); + + // This is only provided by the "Main" repo. + App adb = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(), "siir.es.adbWireless"); + assertAdbMetadata(adb, "Normal"); + assertAdbMetadata(allApps.get("siir.es.adbWireless"), "Normal"); + } + + @Test + public void testCorrectConflictingThenMainThenArchive() throws RepoUpdater.UpdateException { + assertEmpty(); + + updateConflicting(); + updateMain(); + updateArchive(); + + assertExpected(); + } + + @Test + public void testCorrectConflictingThenArchiveThenMain() throws RepoUpdater.UpdateException { + assertEmpty(); + + updateConflicting(); + updateArchive(); + updateMain(); + + assertExpected(); + } + + @Test + public void testCorrectArchiveThenMainThenConflicting() throws RepoUpdater.UpdateException { + assertEmpty(); + + updateArchive(); + updateMain(); + updateConflicting(); + + assertExpected(); + } + + @Test + public void testCorrectArchiveThenConflictingThenMain() throws RepoUpdater.UpdateException { + assertEmpty(); + + updateArchive(); + updateConflicting(); + updateMain(); + + assertExpected(); + } + + @Test + public void testCorrectMainThenArchiveThenConflicting() throws RepoUpdater.UpdateException { + assertEmpty(); + + updateMain(); + updateArchive(); + updateConflicting(); + + assertExpected(); + } + + @Test + public void testCorrectMainThenConflictingThenArchive() throws RepoUpdater.UpdateException { + assertEmpty(); + + updateMain(); + updateConflicting(); + updateArchive(); + + assertExpected(); + } + + /** + * Check that all of the expected apps and apk versions are available in the database. This + * check will take into account the repository the apks came from, to ensure that each + * repository indeed contains the apks that it said it would provide. + */ + private void assertExpected() { + Log.i(TAG, "Asserting all versions of each .apk are in index."); + List repos = RepoProvider.Helper.all(context); + assertEquals("Repos", 3, repos.size()); + + assertMainRepo(repos); + assertMainArchiveRepoMetadata(repos); + assertConflictingRepo(repos); + } + + private void assertMainRepo() { + assertMainRepo(RepoProvider.Helper.all(context)); + } + + /** + * + 2048 (com.uberspot.a2048) + * - Version 1.96 (19) + * - Version 1.95 (18) + * + AdAway (org.adaway) + * - Version 3.0.2 (54) + * - Version 3.0.1 (53) + * - Version 3.0 (52) + * + adbWireless (siir.es.adbWireless) + * - Version 1.5.4 (12) + */ + private void assertMainRepo(List allRepos) { + Repo repo = findRepo(REPO_MAIN, allRepos); + + List apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL); + assertEquals("Apks for main repo", apks.size(), 6); + assertApksExist(apks, "com.uberspot.a2048", new int[]{18, 19}); + assertApksExist(apks, "org.adaway", new int[]{52, 53, 54}); + assertApksExist(apks, "siir.es.adbWireless", new int[]{12}); + + assert2048Metadata(repo, "Normal"); + assertAdAwayMetadata(repo, "Normal"); + assertAdbMetadata(repo, "Normal"); + } + + private void assert2048Metadata(Repo repo, @RepoIdentifier String id) { + App a2048 = AppProvider.Helper.findSpecificApp(context.getContentResolver(), "com.uberspot.a2048", repo.getId(), AppMetadataTable.Cols.ALL); + assert2048Metadata(a2048, id); + } + + /** + * @param id An identifier that we've put in the metadata for each repo to ensure that + * we can identify the metadata is coming from the correct repo. + */ + private void assert2048Metadata(App a2048, @RepoIdentifier String id) { + assertNotNull(a2048); + assertEquals("2048", a2048.name); + assertEquals(String.format("

2048 from %s repo.

", id), a2048.description); + assertEquals(String.format("Puzzle game (%s)", id), a2048.summary); + assertEquals(String.format("https://github.com/uberspot/2048-android?%s", id), a2048.webURL); + assertEquals(String.format("https://github.com/uberspot/2048-android?code&%s", id), a2048.sourceURL); + assertEquals(String.format("https://github.com/uberspot/2048-android/issues?%s", id), a2048.trackerURL); + } + + private void assertAdAwayMetadata(Repo repo, @RepoIdentifier String id) { + App adaway = AppProvider.Helper.findSpecificApp(context.getContentResolver(), "org.adaway", repo.getId(), AppMetadataTable.Cols.ALL); + assertAdAwayMetadata(adaway, id); + } + + /** @see ProperMultiRepoUpdaterTest#assert2048Metadata(Repo, String) */ + private void assertAdAwayMetadata(App adaway, @RepoIdentifier String id) { + assertNotNull(adaway); + assertEquals(String.format("AdAway", id), adaway.name); + assertEquals(String.format("

AdAway from %s repo.

", id), adaway.description); + assertEquals(String.format("Block advertisements (%s)", id), adaway.summary); + assertEquals(String.format("http://sufficientlysecure.org/index.php/adaway?%s", id), adaway.webURL); + assertEquals(String.format("https://github.com/dschuermann/ad-away?%s", id), adaway.sourceURL); + assertEquals(String.format("https://github.com/dschuermann/ad-away/issues?%s", id), adaway.trackerURL); + assertEquals(String.format("https://github.com/dschuermann/ad-away/raw/HEAD/CHANGELOG?%s", id), adaway.changelogURL); + assertEquals(String.format("http://sufficientlysecure.org/index.php/adaway?%s", id), adaway.donateURL); + assertEquals(String.format("369138", id), adaway.flattrID); + } + + private void assertAdbMetadata(Repo repo, @RepoIdentifier String id) { + App adb = AppProvider.Helper.findSpecificApp(context.getContentResolver(), "siir.es.adbWireless", repo.getId(), AppMetadataTable.Cols.ALL); + assertAdbMetadata(adb, id); + } + + /** @see ProperMultiRepoUpdaterTest#assert2048Metadata(Repo, String) */ + private void assertAdbMetadata(App adb, @RepoIdentifier String id) { + assertNotNull(adb); + assertEquals("adbWireless", adb.name); + assertEquals(String.format("

adbWireless from %s repo.

", id), adb.description); + assertEquals(String.format("Wireless adb (%s)", id), adb.summary); + assertEquals(String.format("https://adbwireless.example.com?%s", id), adb.webURL); + assertEquals(String.format("https://adbwireless.example.com/source?%s", id), adb.sourceURL); + assertEquals(String.format("https://adbwireless.example.com/issues?%s", id), adb.trackerURL); + } + + private void assertCalendarMetadata(Repo repo, @RepoIdentifier String id) { + App calendar = AppProvider.Helper.findSpecificApp(context.getContentResolver(), "org.dgtale.icsimport", repo.getId(), AppMetadataTable.Cols.ALL); + assertCalendarMetadata(calendar, id); + } + + /** @see ProperMultiRepoUpdaterTest#assert2048Metadata(Repo, String) */ + private void assertCalendarMetadata(App calendar, @RepoIdentifier String id) { + assertNotNull(calendar); + assertEquals("Add to calendar", calendar.name); + assertEquals(String.format("

Add to calendar from %s repo.

", id), calendar.description); + assertEquals(String.format("Import .ics files into calendar (%s)", id), calendar.summary); + assertEquals(String.format("https://github.com/danielegobbetti/ICSImport/blob/HEAD/README.md?%s", id), calendar.webURL); + assertEquals(String.format("https://github.com/danielegobbetti/ICSImport?%s", id), calendar.sourceURL); + assertEquals(String.format("https://github.com/danielegobbetti/ICSImport/issues?%s", id), calendar.trackerURL); + assertEquals("2225390", calendar.flattrID); + } + + private void assertMainArchiveRepoMetadata() { + assertMainArchiveRepoMetadata(RepoProvider.Helper.all(context)); + } + + /** + * + AdAway (org.adaway) + * - Version 2.9.2 (51) + * - Version 2.9.1 (50) + * - Version 2.9 (49) + * - Version 2.8.1 (48) + * - Version 2.8 (47) + * - Version 2.7 (46) + * - Version 2.6 (45) + * - Version 2.3 (42) + * - Version 2.1 (40) + * - Version 1.37 (38) + * - Version 1.36 (37) + * - Version 1.35 (36) + * - Version 1.34 (35) + */ + private void assertMainArchiveRepoMetadata(List allRepos) { + Repo repo = findRepo(REPO_ARCHIVE, allRepos); + + List apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL); + assertEquals("Apks for main archive repo", 13, apks.size()); + assertApksExist(apks, "org.adaway", new int[]{35, 36, 37, 38, 40, 42, 45, 46, 47, 48, 49, 50, 51}); + + assertAdAwayMetadata(repo, "Normal"); + } + + private void assertConflictingRepo() { + assertConflictingRepo(RepoProvider.Helper.all(context)); + } + + /** + * + AdAway (org.adaway) + * - Version 3.0.1 (53) * + * - Version 3.0 (52) * + * - Version 2.9.2 (51) * + * - Version 2.2.1 (50) * + * + Add to calendar (org.dgtale.icsimport) + * - Version 1.2 (3) + * - Version 1.1 (2) + */ + private void assertConflictingRepo(List allRepos) { + Repo repo = findRepo(REPO_CONFLICTING, allRepos); + + List apks = ApkProvider.Helper.findByRepo(context, repo, Schema.ApkTable.Cols.ALL); + assertEquals("Apks for conflicting repo", 6, apks.size()); + assertApksExist(apks, "org.adaway", new int[]{50, 51, 52, 53}); + assertApksExist(apks, "org.dgtale.icsimport", new int[]{2, 3}); + + assertAdAwayMetadata(repo, "Conflicting"); + assertCalendarMetadata(repo, "Conflicting"); + } + +} diff --git a/app/src/test/java/org/fdroid/fdroid/RepoXMLHandlerTest.java b/app/src/test/java/org/fdroid/fdroid/updater/RepoXMLHandlerTest.java similarity index 99% rename from app/src/test/java/org/fdroid/fdroid/RepoXMLHandlerTest.java rename to app/src/test/java/org/fdroid/fdroid/updater/RepoXMLHandlerTest.java index 2f40767db..12751a40a 100644 --- a/app/src/test/java/org/fdroid/fdroid/RepoXMLHandlerTest.java +++ b/app/src/test/java/org/fdroid/fdroid/updater/RepoXMLHandlerTest.java @@ -20,12 +20,14 @@ * MA 02110-1301, USA. */ -package org.fdroid.fdroid; +package org.fdroid.fdroid.updater; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; +import org.fdroid.fdroid.BuildConfig; +import org.fdroid.fdroid.RepoXMLHandler; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.Repo; diff --git a/app/src/test/resources/multiRepo.archive.jar b/app/src/test/resources/multiRepo.archive.jar index f5c505c07..f1742ceb8 100644 Binary files a/app/src/test/resources/multiRepo.archive.jar and b/app/src/test/resources/multiRepo.archive.jar differ diff --git a/app/src/test/resources/multiRepo.conflicting.jar b/app/src/test/resources/multiRepo.conflicting.jar index 6d26f65d3..a9f5beeaf 100644 Binary files a/app/src/test/resources/multiRepo.conflicting.jar and b/app/src/test/resources/multiRepo.conflicting.jar differ diff --git a/app/src/test/resources/multiRepo.normal.jar b/app/src/test/resources/multiRepo.normal.jar index 6a53256eb..d42de35bf 100644 Binary files a/app/src/test/resources/multiRepo.normal.jar and b/app/src/test/resources/multiRepo.normal.jar differ