Merge branch 'issue-568--removing-apks' into 'master'
Issue 568 removing apks Closes #568 See merge request !564
This commit is contained in:
		
						commit
						78f35db060
					
				| @ -139,7 +139,7 @@ public class IndexV1Updater extends RepoUpdater { | ||||
|      * and {@link Apk} instances.  This uses {@link RepoPersister}  to add the apps | ||||
|      * and packages to the database in {@link RepoPersister#saveToDb(App, List)} | ||||
|      * to write the {@link Repo}, and commit the whole thing in | ||||
|      * {@link RepoPersister#commit(ContentValues)}.  One confusing thing about this | ||||
|      * {@link RepoPersister#commit(ContentValues, long)}.  One confusing thing about this | ||||
|      * whole process is that {@link RepoPersister} needs to first create and entry | ||||
|      * in the database, then fetch the ID from the database to populate | ||||
|      * {@link Repo#id}.  That has to happen first, then the rest of the {@code Repo} | ||||
| @ -265,7 +265,7 @@ public class IndexV1Updater extends RepoUpdater { | ||||
|         if (repo.mirrors != null && repo.mirrors.length > 0) { | ||||
|             contentValues.put(Schema.RepoTable.Cols.MIRRORS, Utils.serializeCommaSeparatedString(repo.mirrors)); | ||||
|         } | ||||
|         repoPersister.commit(contentValues); | ||||
|         repoPersister.commit(contentValues, repo.getId()); | ||||
|         profiler.log("Persited to database."); | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -280,7 +280,7 @@ public class RepoUpdater { | ||||
|     private void commitToDb() throws UpdateException { | ||||
|         Log.i(TAG, "Repo signature verified, saving app metadata to database."); | ||||
|         notifyCommittingToDb(); | ||||
|         persister.commit(repoDetailsToSave); | ||||
|         persister.commit(repoDetailsToSave, repo.getId()); | ||||
|     } | ||||
| 
 | ||||
|     private void assertSigningCertFromXmlCorrect() throws SigningException { | ||||
|  | ||||
| @ -94,17 +94,6 @@ public class ApkProvider extends FDroidProvider { | ||||
|             return findApkFromAnyRepo(context, packageName, versionCode, signature, Cols.ALL); | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Find all apks for a particular app, but limit it to those originating from the | ||||
|          * specified repo. | ||||
|          */ | ||||
|         public static List<Apk> findByUri(Context context, Repo repo, List<App> apps, String[] projection) { | ||||
|             ContentResolver resolver = context.getContentResolver(); | ||||
|             final Uri uri = getContentUriForApps(repo, apps); | ||||
|             Cursor cursor = resolver.query(uri, projection, null, null, null); | ||||
|             return cursorToList(cursor); | ||||
|         } | ||||
| 
 | ||||
|         public static Apk findApkFromAnyRepo(Context context, String packageName, int versionCode, | ||||
|                                              @Nullable String signature, String[] projection) { | ||||
|             final Uri uri = getApkFromAnyRepoUri(packageName, versionCode, signature); | ||||
| @ -137,36 +126,6 @@ public class ApkProvider extends FDroidProvider { | ||||
|             return cursorToList(cursor); | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Returns apks in the database, which have the same packageName and version as | ||||
|          * one of the apks in the "apks" argument. | ||||
|          */ | ||||
|         public static List<Apk> knownApks(Context context, List<Apk> apks, String[] fields) { | ||||
|             if (apks.isEmpty()) { | ||||
|                 return new ArrayList<>(); | ||||
|             } | ||||
| 
 | ||||
|             List<Apk> knownApks = new ArrayList<>(); | ||||
|             if (apks.size() > ApkProvider.MAX_APKS_TO_QUERY) { | ||||
|                 int middle = apks.size() / 2; | ||||
|                 List<Apk> apks1 = apks.subList(0, middle); | ||||
|                 List<Apk> apks2 = apks.subList(middle, apks.size()); | ||||
|                 knownApks.addAll(knownApks(context, apks1, fields)); | ||||
|                 knownApks.addAll(knownApks(context, apks2, fields)); | ||||
|             } else { | ||||
|                 knownApks.addAll(knownApksSafe(context, apks, fields)); | ||||
|             } | ||||
|             return knownApks; | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         private static List<Apk> knownApksSafe(final Context context, final List<Apk> apks, final String[] fields) { | ||||
|             ContentResolver resolver = context.getContentResolver(); | ||||
|             final Uri uri = getContentUri(apks); | ||||
|             Cursor cursor = resolver.query(uri, fields, null, null, null); | ||||
|             return cursorToList(cursor); | ||||
|         } | ||||
| 
 | ||||
|         public static List<Apk> findByRepo(Context context, Repo repo, String[] fields) { | ||||
|             ContentResolver resolver = context.getContentResolver(); | ||||
|             final Uri uri = getRepoUri(repo.getId()); | ||||
| @ -206,9 +165,7 @@ public class ApkProvider extends FDroidProvider { | ||||
|     private static final int CODE_PACKAGE = CODE_SINGLE + 1; | ||||
|     private static final int CODE_REPO = CODE_PACKAGE + 1; | ||||
|     private static final int CODE_APKS = CODE_REPO + 1; | ||||
|     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; | ||||
|     private static final int CODE_APK_ROW_ID = CODE_APKS + 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; | ||||
| 
 | ||||
| @ -218,8 +175,6 @@ public class ApkProvider extends FDroidProvider { | ||||
|     private static final String PATH_APKS = "apks"; | ||||
|     private static final String PATH_APP = "app"; | ||||
|     private static final String PATH_REPO      = "repo"; | ||||
|     private static final String PATH_REPO_APPS = "repo-apps"; | ||||
|     protected static final String PATH_REPO_APK  = "repo-apk"; | ||||
|     private static final String PATH_APK_ROW_ID = "apk-rowId"; | ||||
| 
 | ||||
|     private static final UriMatcher MATCHER = new UriMatcher(-1); | ||||
| @ -238,8 +193,6 @@ public class ApkProvider extends FDroidProvider { | ||||
|         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); | ||||
|         MATCHER.addURI(getAuthority(), PATH_REPO_APK + "/#/*", CODE_REPO_APK); | ||||
|         MATCHER.addURI(getAuthority(), PATH_APK_ROW_ID + "/#", CODE_APK_ROW_ID); | ||||
|         MATCHER.addURI(getAuthority(), null, CODE_LIST); | ||||
|     } | ||||
| @ -293,51 +246,6 @@ public class ApkProvider extends FDroidProvider { | ||||
|         return builder.build(); | ||||
|     } | ||||
| 
 | ||||
|     public static Uri getContentUriForApps(Repo repo, List<App> apps) { | ||||
|         return getContentUri() | ||||
|             .buildUpon() | ||||
|             .appendPath(PATH_REPO_APPS) | ||||
|             .appendPath(Long.toString(repo.id)) | ||||
|             .appendPath(buildAppString(apps)) | ||||
|             .build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Intentionally left protected because it will break if apks is larger than | ||||
|      * {@link org.fdroid.fdroid.data.ApkProvider#MAX_APKS_TO_QUERY}. Instead of using | ||||
|      * this directly, think about using | ||||
|      * {@link ApkProvider.Helper#knownApks(android.content.Context, java.util.List, String[])} | ||||
|      */ | ||||
|     static Uri getContentUri(List<Apk> apks) { | ||||
|         return getContentUri().buildUpon() | ||||
|                 .appendPath(PATH_APKS) | ||||
|                 .appendPath(buildApkString(apks)) | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
|     protected static String buildApkString(List<Apk> apks) { | ||||
|         StringBuilder builder = new StringBuilder(); | ||||
|         for (int i = 0; i < apks.size(); i++) { | ||||
|             if (i != 0) { | ||||
|                 builder.append(','); | ||||
|             } | ||||
|             final Apk apk = apks.get(i); | ||||
|             builder.append(apk.appId).append(':').append(apk.versionCode); | ||||
|         } | ||||
|         return builder.toString(); | ||||
|     } | ||||
| 
 | ||||
|     private static String buildAppString(List<App> apks) { | ||||
|         StringBuilder builder = new StringBuilder(); | ||||
|         for (int i = 0; i < apks.size(); i++) { | ||||
|             if (i != 0) { | ||||
|                 builder.append(','); | ||||
|             } | ||||
|             builder.append(apks.get(i).packageName); | ||||
|         } | ||||
|         return builder.toString(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected String getTableName() { | ||||
|         return ApkTable.NAME; | ||||
| @ -470,10 +378,6 @@ public class ApkProvider extends FDroidProvider { | ||||
|         return new QuerySelection(selection, args); | ||||
|     } | ||||
| 
 | ||||
|     private QuerySelection queryRepoApps(long repoId, String packageNames) { | ||||
|         return queryRepo(repoId).add(AppProvider.queryPackageNames(packageNames, "pkg." + PackageTable.Cols.PACKAGE_NAME)); | ||||
|     } | ||||
| 
 | ||||
|     protected QuerySelection queryApks(String apkKeys) { | ||||
|         return queryApks(apkKeys, true); | ||||
|     } | ||||
| @ -547,11 +451,6 @@ public class ApkProvider extends FDroidProvider { | ||||
|                 query = query.add(queryRepo(Long.parseLong(uri.getLastPathSegment()))); | ||||
|                 break; | ||||
| 
 | ||||
|             case CODE_REPO_APPS: | ||||
|                 List<String> pathSegments = uri.getPathSegments(); | ||||
|                 query = query.add(queryRepoApps(Long.parseLong(pathSegments.get(1)), pathSegments.get(2))); | ||||
|                 break; | ||||
| 
 | ||||
|             default: | ||||
|                 Log.e(TAG, "Invalid URI for apk content provider: " + uri); | ||||
|                 throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); | ||||
| @ -611,12 +510,6 @@ public class ApkProvider extends FDroidProvider { | ||||
|                 query = query.add(queryApks(uri.getLastPathSegment(), false)); | ||||
|                 break; | ||||
| 
 | ||||
|             // TODO: Add tests for this. | ||||
|             case CODE_REPO_APK: | ||||
|                 List<String> pathSegments = uri.getPathSegments(); | ||||
|                 query = query.add(queryRepo(Long.parseLong(pathSegments.get(1)))).add(queryApks(pathSegments.get(2))); | ||||
|                 break; | ||||
| 
 | ||||
|             default: | ||||
|                 Log.e(TAG, "Invalid URI for apk content provider: " + uri); | ||||
|                 throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); | ||||
| @ -633,10 +526,7 @@ public class ApkProvider extends FDroidProvider { | ||||
|         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); | ||||
|     } | ||||
| 
 | ||||
|     protected int performUpdateUnchecked(Uri uri, ContentValues values, String where, String[] whereArgs) { | ||||
|         validateFields(Cols.ALL, values); | ||||
|         removeFieldsFromOtherTables(values); | ||||
| 
 | ||||
|  | ||||
| @ -24,8 +24,10 @@ import org.fdroid.fdroid.data.Schema.RepoTable; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.HashMap; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -12,13 +12,39 @@ import org.fdroid.fdroid.data.Schema.CategoryTable; | ||||
| import org.fdroid.fdroid.data.Schema.CategoryTable.Cols; | ||||
| import org.fdroid.fdroid.data.Schema.PackageTable; | ||||
| 
 | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| public class CategoryProvider extends FDroidProvider { | ||||
| 
 | ||||
|     public static final class Helper { | ||||
|         private Helper() { | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * During repo updates, each app needs to know the ID of each category it belongs to. | ||||
|          * This results in lots of database lookups, usually at least one for each app, sometimes more. | ||||
|          * To improve performance, this caches the association between categories and their database IDs. | ||||
|          * | ||||
|          * It can stay around for the entire F-Droid process, even across multiple repo updates, as we | ||||
|          * don't actually remove data from the categories table. | ||||
|          */ | ||||
|         private static final Map<String, Long> KNOWN_CATEGORIES = new HashMap<>(); | ||||
| 
 | ||||
|         /** | ||||
|          * Used by tests to clear that the "Category -> ID" cache (used to prevent excessive disk reads). | ||||
|          */ | ||||
|         static void clearCategoryIdCache() { | ||||
|             KNOWN_CATEGORIES.clear(); | ||||
|         } | ||||
| 
 | ||||
|         public static long ensureExists(Context context, String category) { | ||||
|             // Check our in-memory cache to potentially prevent a trip to the database (and hence disk). | ||||
|             String lowerCaseCategory = category.toLowerCase(); | ||||
|             if (KNOWN_CATEGORIES.containsKey(lowerCaseCategory)) { | ||||
|                 return KNOWN_CATEGORIES.get(lowerCaseCategory); | ||||
|             } | ||||
| 
 | ||||
|             long id = getCategoryId(context, category); | ||||
|             if (id <= 0) { | ||||
|                 ContentValues values = new ContentValues(1); | ||||
| @ -26,10 +52,13 @@ public class CategoryProvider extends FDroidProvider { | ||||
|                 Uri uri = context.getContentResolver().insert(getContentUri(), values); | ||||
|                 id = Long.parseLong(uri.getLastPathSegment()); | ||||
|             } | ||||
| 
 | ||||
|             KNOWN_CATEGORIES.put(lowerCaseCategory, id); | ||||
| 
 | ||||
|             return id; | ||||
|         } | ||||
| 
 | ||||
|         public static long getCategoryId(Context context, String category) { | ||||
|         private static long getCategoryId(Context context, String category) { | ||||
|             String[] projection = new String[]{Cols.ROW_ID}; | ||||
|             Cursor cursor = context.getContentResolver().query(getCategoryUri(category), projection, | ||||
|                     null, null, null); | ||||
|  | ||||
| @ -7,7 +7,6 @@ import android.content.OperationApplicationException; | ||||
| import android.net.Uri; | ||||
| import android.os.RemoteException; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| 
 | ||||
| import org.fdroid.fdroid.CompatibilityChecker; | ||||
| import org.fdroid.fdroid.RepoUpdater; | ||||
| @ -18,7 +17,6 @@ import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| @SuppressWarnings("LineLength") | ||||
| public class RepoPersister { | ||||
| 
 | ||||
|     private static final String TAG = "RepoPersister"; | ||||
| @ -66,9 +64,9 @@ public class RepoPersister { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void commit(ContentValues repoDetailsToSave) throws RepoUpdater.UpdateException { | ||||
|     public void commit(ContentValues repoDetailsToSave, long repoIdToCommit) throws RepoUpdater.UpdateException { | ||||
|         flushBufferToDb(); | ||||
|         TempAppProvider.Helper.commitAppsAndApks(context); | ||||
|         TempAppProvider.Helper.commitAppsAndApks(context, repoIdToCommit); | ||||
|         RepoProvider.Helper.update(context, repo, repoDetailsToSave); | ||||
|     } | ||||
| 
 | ||||
| @ -79,12 +77,12 @@ public class RepoPersister { | ||||
|             // the index was signed with until we've finished reading it - and we don't | ||||
|             // want to put stuff in the real database until we are sure it is from a | ||||
|             // trusted source. It also helps performance as it is done via an in-memory database. | ||||
|             TempAppProvider.Helper.init(context); | ||||
|             TempAppProvider.Helper.init(context, repo.getId()); | ||||
|             hasBeenInitialized = true; | ||||
|         } | ||||
| 
 | ||||
|         if (apksToSave.size() > 0 || appsToSave.size() > 0) { | ||||
|             Utils.debugLog(TAG, "Flushing details of up to " + MAX_APP_BUFFER + " apps and their packages to the database."); | ||||
|             Utils.debugLog(TAG, "Flushing details of up to " + MAX_APP_BUFFER + " apps/packages to the database."); | ||||
|             Map<String, Long> appIds = flushAppsToDbInBatch(); | ||||
|             flushApksToDbInBatch(appIds); | ||||
|             apksToSave.clear(); | ||||
| @ -103,12 +101,7 @@ public class RepoPersister { | ||||
| 
 | ||||
|         calcApkCompatibilityFlags(apksToSaveList); | ||||
| 
 | ||||
|         ArrayList<ContentProviderOperation> apkOperations = new ArrayList<>(); | ||||
|         ContentProviderOperation clearOrphans = deleteOrphanedApks(appsToSave, apksToSave); | ||||
|         if (clearOrphans != null) { | ||||
|             apkOperations.add(clearOrphans); | ||||
|         } | ||||
|         apkOperations.addAll(insertOrUpdateApks(apksToSaveList)); | ||||
|         ArrayList<ContentProviderOperation> apkOperations = insertApks(apksToSaveList); | ||||
| 
 | ||||
|         try { | ||||
|             context.getContentResolver().applyBatch(TempApkProvider.getAuthority(), apkOperations); | ||||
| @ -123,7 +116,7 @@ public class RepoPersister { | ||||
|      * can be returned and the relevant apks can be joined to the app table correctly. | ||||
|      */ | ||||
|     private Map<String, Long> flushAppsToDbInBatch() throws RepoUpdater.UpdateException { | ||||
|         ArrayList<ContentProviderOperation> appOperations = insertOrUpdateApps(appsToSave); | ||||
|         ArrayList<ContentProviderOperation> appOperations = insertApps(appsToSave); | ||||
| 
 | ||||
|         try { | ||||
|             context.getContentResolver().applyBatch(TempAppProvider.getAuthority(), appOperations); | ||||
| @ -144,7 +137,12 @@ public class RepoPersister { | ||||
|         for (App app : apps) { | ||||
|             packageNames.add(app.packageName); | ||||
|         } | ||||
|         String[] projection = {Schema.AppMetadataTable.Cols.ROW_ID, Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME}; | ||||
| 
 | ||||
|         String[] projection = { | ||||
|                 Schema.AppMetadataTable.Cols.ROW_ID, | ||||
|                 Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME, | ||||
|         }; | ||||
| 
 | ||||
|         List<App> fromDb = TempAppProvider.Helper.findByPackageNames(context, packageNames, repo.id, projection); | ||||
| 
 | ||||
|         Map<String, Long> ids = new HashMap<>(fromDb.size()); | ||||
| @ -154,138 +152,27 @@ public class RepoPersister { | ||||
|         return ids; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Depending on whether the {@link App}s have been added to the database previously, this | ||||
|      * will queue up an update or an insert {@link ContentProviderOperation} for each app. | ||||
|      */ | ||||
|     private ArrayList<ContentProviderOperation> insertOrUpdateApps(List<App> apps) { | ||||
|     private ArrayList<ContentProviderOperation> insertApps(List<App> apps) { | ||||
|         ArrayList<ContentProviderOperation> operations = new ArrayList<>(apps.size()); | ||||
|         for (App app : apps) { | ||||
|             if (isAppInDatabase(app)) { | ||||
|                 operations.add(updateExistingApp(app)); | ||||
|             } else { | ||||
|                 operations.add(insertNewApp(app)); | ||||
|             } | ||||
|             ContentValues values = app.toContentValues(); | ||||
|             Uri uri = TempAppProvider.getContentUri(); | ||||
|             operations.add(ContentProviderOperation.newInsert(uri).withValues(values).build()); | ||||
|         } | ||||
|         return operations; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Depending on whether the .apks have been added to the database previously, this | ||||
|      * will queue up an update or an insert {@link ContentProviderOperation} for each package. | ||||
|      */ | ||||
|     private ArrayList<ContentProviderOperation> insertOrUpdateApks(List<Apk> packages) { | ||||
|         String[] projection = new String[]{ | ||||
|                 Schema.ApkTable.Cols.Package.PACKAGE_NAME, | ||||
|                 Schema.ApkTable.Cols.VERSION_CODE, | ||||
|                 Schema.ApkTable.Cols.REPO_ID, | ||||
|                 Schema.ApkTable.Cols.APP_ID, | ||||
|         }; | ||||
|         List<Apk> existingApks = ApkProvider.Helper.knownApks(context, packages, projection); | ||||
|     private ArrayList<ContentProviderOperation> insertApks(List<Apk> packages) { | ||||
|         ArrayList<ContentProviderOperation> operations = new ArrayList<>(packages.size()); | ||||
|         for (Apk apk : packages) { | ||||
|             boolean exists = false; | ||||
|             for (Apk existing : existingApks) { | ||||
|                 if (existing.repoId == apk.repoId && existing.packageName.equals(apk.packageName) && existing.versionCode == apk.versionCode) { | ||||
|                     exists = true; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (exists) { | ||||
|                 operations.add(updateExistingApk(apk)); | ||||
|             } else { | ||||
|                 operations.add(insertNewApk(apk)); | ||||
|             } | ||||
|             ContentValues values = apk.toContentValues(); | ||||
|             Uri uri = TempApkProvider.getContentUri(); | ||||
|             operations.add(ContentProviderOperation.newInsert(uri).withValues(values).build()); | ||||
|         } | ||||
| 
 | ||||
|         return operations; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates an update {@link ContentProviderOperation} for the {@link App} in question. | ||||
|      * <strong>Does not do any checks to see if the app already exists or not.</strong> | ||||
|      */ | ||||
|     private ContentProviderOperation updateExistingApp(App app) { | ||||
|         Uri uri = TempAppProvider.getSpecificTempAppUri(app.packageName, app.repoId); | ||||
|         return ContentProviderOperation.newUpdate(uri).withValues(app.toContentValues()).build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates an insert {@link ContentProviderOperation} for the {@link App} in question. | ||||
|      * <strong>Does not do any checks to see if the app already exists or not.</strong> | ||||
|      */ | ||||
|     private ContentProviderOperation insertNewApp(App app) { | ||||
|         ContentValues values = app.toContentValues(); | ||||
|         Uri uri = TempAppProvider.getContentUri(); | ||||
|         return ContentProviderOperation.newInsert(uri).withValues(values).build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Looks in the database to see which apps we already know about. Only | ||||
|      * returns ids of apps that are in the database if they are in the "apps" | ||||
|      * array. | ||||
|      */ | ||||
|     private boolean isAppInDatabase(App app) { | ||||
|         String[] fields = {Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME}; | ||||
|         App found = AppProvider.Helper.findSpecificApp(context.getContentResolver(), app.packageName, repo.id, fields); | ||||
|         return found != null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates an update {@link ContentProviderOperation} for the {@link Apk} in question. | ||||
|      * <strong>Does not do any checks to see if the apk already exists or not.</strong> | ||||
|      */ | ||||
|     private ContentProviderOperation updateExistingApk(final Apk apk) { | ||||
|         Uri uri = TempApkProvider.getApkUri(apk); | ||||
|         ContentValues values = apk.toContentValues(); | ||||
|         return ContentProviderOperation.newUpdate(uri).withValues(values).build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Creates an insert {@link ContentProviderOperation} for the {@link Apk} in question. | ||||
|      * <strong>Does not do any checks to see if the apk already exists or not.</strong> | ||||
|      */ | ||||
|     private ContentProviderOperation insertNewApk(final Apk apk) { | ||||
|         ContentValues values = apk.toContentValues(); | ||||
|         Uri uri = TempApkProvider.getContentUri(); | ||||
|         return ContentProviderOperation.newInsert(uri).withValues(values).build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Finds all apks from the repo we are currently updating, that belong to the specified app, | ||||
|      * and delete them as they are no longer provided by that repo. | ||||
|      */ | ||||
|     @Nullable | ||||
|     private ContentProviderOperation deleteOrphanedApks(List<App> apps, Map<String, List<Apk>> packages) { | ||||
|         String[] projection = new String[]{Schema.ApkTable.Cols.Package.PACKAGE_NAME, Schema.ApkTable.Cols.VERSION_CODE}; | ||||
|         List<Apk> existing = ApkProvider.Helper.findByUri(context, repo, apps, projection); | ||||
|         List<Apk> toDelete = new ArrayList<>(); | ||||
| 
 | ||||
|         for (Apk existingApk : existing) { | ||||
|             boolean shouldStay = false; | ||||
| 
 | ||||
|             if (packages.containsKey(existingApk.packageName)) { | ||||
|                 for (Apk newApk : packages.get(existingApk.packageName)) { | ||||
|                     if (newApk.versionCode == existingApk.versionCode) { | ||||
|                         shouldStay = true; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (!shouldStay) { | ||||
|                 toDelete.add(existingApk); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (toDelete.size() == 0) { | ||||
|             return null; | ||||
|         } | ||||
|         Uri uri = TempApkProvider.getApksUri(repo, toDelete); | ||||
|         return ContentProviderOperation.newDelete(uri).build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This cannot be offloaded to the database (as we did with the query which | ||||
|      * updates apps, depending on whether their apks are compatible or not). | ||||
|  | ||||
| @ -85,6 +85,8 @@ public interface Schema { | ||||
|         String NAME = "fdroid_categoryAppMetadataJoin"; | ||||
| 
 | ||||
|         interface Cols { | ||||
|             String ROW_ID = "rowid"; | ||||
| 
 | ||||
|             /** | ||||
|              * Foreign key to {@link AppMetadataTable}. | ||||
|              * @see AppMetadataTable | ||||
| @ -100,7 +102,7 @@ public interface Schema { | ||||
|             /** | ||||
|              * @see AppMetadataTable.Cols#ALL_COLS | ||||
|              */ | ||||
|             String[] ALL_COLS = {APP_METADATA_ID, CATEGORY_ID}; | ||||
|             String[] ALL_COLS = {ROW_ID, APP_METADATA_ID, CATEGORY_ID}; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -7,8 +7,7 @@ import android.database.sqlite.SQLiteDatabase; | ||||
| import android.net.Uri; | ||||
| 
 | ||||
| import org.fdroid.fdroid.data.Schema.ApkTable; | ||||
| 
 | ||||
| import java.util.List; | ||||
| import org.fdroid.fdroid.data.Schema.ApkTable.Cols; | ||||
| 
 | ||||
| /** | ||||
|  * This class does all of its operations in a temporary sqlite table. | ||||
| @ -27,10 +26,9 @@ public class TempApkProvider extends ApkProvider { | ||||
|     private static final UriMatcher MATCHER = new UriMatcher(-1); | ||||
| 
 | ||||
|     static { | ||||
|         MATCHER.addURI(getAuthority(), PATH_INIT, CODE_INIT); | ||||
|         MATCHER.addURI(getAuthority(), PATH_INIT + "/#", CODE_INIT); | ||||
|         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); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @ -51,24 +49,6 @@ public class TempApkProvider extends ApkProvider { | ||||
|         return Uri.parse("content://" + getAuthority()); | ||||
|     } | ||||
| 
 | ||||
|     public static Uri getApkUri(Apk apk) { | ||||
|         return getContentUri() | ||||
|                 .buildUpon() | ||||
|                 .appendPath(PATH_APK_FROM_REPO) | ||||
|                 .appendPath(Long.toString(apk.appId)) | ||||
|                 .appendPath(Integer.toString(apk.versionCode)) | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
|     public static Uri getApksUri(Repo repo, List<Apk> apks) { | ||||
|         return getContentUri() | ||||
|                 .buildUpon() | ||||
|                 .appendPath(PATH_REPO_APK) | ||||
|                 .appendPath(Long.toString(repo.id)) | ||||
|                 .appendPath(buildApkString(apks)) | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
|     public static class Helper { | ||||
| 
 | ||||
|         /** | ||||
| @ -76,12 +56,15 @@ public class TempApkProvider extends ApkProvider { | ||||
|          * table and populates it with all the data from the real apk provider table. | ||||
|          * | ||||
|          * This is package local because it must be invoked after | ||||
|          * {@link org.fdroid.fdroid.data.TempAppProvider.Helper#init(Context)}. Due to this | ||||
|          * {@link org.fdroid.fdroid.data.TempAppProvider.Helper#init(Context, long)}. Due to this | ||||
|          * dependence, that method invokes this one itself, rather than leaving it to the | ||||
|          * {@link RepoPersister}. | ||||
|          */ | ||||
|         static void init(Context context) { | ||||
|             Uri uri = Uri.withAppendedPath(getContentUri(), PATH_INIT); | ||||
|         static void init(Context context, long repoIdToUpdate) { | ||||
|             Uri uri = getContentUri().buildUpon() | ||||
|                     .appendPath(PATH_INIT) | ||||
|                     .appendPath(Long.toString(repoIdToUpdate)) | ||||
|                     .build(); | ||||
|             context.getContentResolver().insert(uri, new ContentValues()); | ||||
|         } | ||||
|     } | ||||
| @ -89,7 +72,7 @@ public class TempApkProvider extends ApkProvider { | ||||
|     @Override | ||||
|     public Uri insert(Uri uri, ContentValues values) { | ||||
|         if (MATCHER.match(uri) == CODE_INIT) { | ||||
|             initTable(); | ||||
|             initTable(Long.parseLong(uri.getLastPathSegment())); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
| @ -98,39 +81,25 @@ public class TempApkProvider extends ApkProvider { | ||||
| 
 | ||||
|     @Override | ||||
|     public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { | ||||
|         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); | ||||
|         throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int delete(Uri uri, String where, String[] whereArgs) { | ||||
|         if (MATCHER.match(uri) != CODE_REPO_APK) { | ||||
|             throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); | ||||
|         } | ||||
| 
 | ||||
|         List<String> pathSegments = uri.getPathSegments(); | ||||
|         QuerySelection query = new QuerySelection(where, whereArgs) | ||||
|                 .add(queryRepo(Long.parseLong(pathSegments.get(1)), false)) | ||||
|                 .add(queryApks(pathSegments.get(2), false)); | ||||
| 
 | ||||
|         int rowsAffected = db().delete(getTableName(), query.getSelection(), query.getArgs()); | ||||
|         if (!isApplyingBatch()) { | ||||
|             getContext().getContentResolver().notifyChange(uri, null); | ||||
|         } | ||||
|         return rowsAffected; | ||||
| 
 | ||||
|         throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); | ||||
|     } | ||||
| 
 | ||||
|     private void initTable() { | ||||
|     private void initTable(long repoIdBeingUpdated) { | ||||
|         final SQLiteDatabase db = db(); | ||||
|         final String memoryDbName = TempAppProvider.DB; | ||||
|         db.execSQL(DBHelper.CREATE_TABLE_APK.replaceFirst(Schema.ApkTable.NAME, memoryDbName + "." + getTableName())); | ||||
|         db.execSQL(TempAppProvider.copyData(Schema.ApkTable.Cols.ALL_COLS, Schema.ApkTable.NAME, memoryDbName + "." + getTableName())); | ||||
|         db.execSQL("CREATE INDEX IF NOT EXISTS " + memoryDbName + ".apk_appId on " + getTableName() + " (" + ApkTable.Cols.APP_ID + ");"); | ||||
|         db.execSQL("CREATE INDEX IF NOT EXISTS " + memoryDbName + ".apk_compatible ON " + getTableName() + " (" + ApkTable.Cols.IS_COMPATIBLE + ");"); | ||||
|         db.execSQL(DBHelper.CREATE_TABLE_APK.replaceFirst(ApkTable.NAME, memoryDbName + "." + getTableName())); | ||||
| 
 | ||||
|         String where = ApkTable.NAME + "." + Cols.REPO_ID + " != ?"; | ||||
|         String[] whereArgs = new String[]{Long.toString(repoIdBeingUpdated)}; | ||||
|         db.execSQL(TempAppProvider.copyData(Cols.ALL_COLS, ApkTable.NAME, memoryDbName + "." + getTableName(), where), whereArgs); | ||||
| 
 | ||||
|         db.execSQL("CREATE INDEX IF NOT EXISTS " + memoryDbName + ".apk_appId on " + getTableName() + " (" + Cols.APP_ID + ");"); | ||||
|         db.execSQL("CREATE INDEX IF NOT EXISTS " + memoryDbName + ".apk_compatible ON " + getTableName() + " (" + Cols.IS_COMPATIBLE + ");"); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -8,7 +8,6 @@ import android.database.sqlite.SQLiteDatabase; | ||||
| import android.database.sqlite.SQLiteException; | ||||
| import android.net.Uri; | ||||
| import android.text.TextUtils; | ||||
| import org.fdroid.fdroid.Utils; | ||||
| import org.fdroid.fdroid.data.Schema.ApkTable; | ||||
| import org.fdroid.fdroid.data.Schema.AppMetadataTable; | ||||
| import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols; | ||||
| @ -43,8 +42,8 @@ public class TempAppProvider extends AppProvider { | ||||
|     private static final UriMatcher MATCHER = new UriMatcher(-1); | ||||
| 
 | ||||
|     static { | ||||
|         MATCHER.addURI(getAuthority(), PATH_INIT, CODE_INIT); | ||||
|         MATCHER.addURI(getAuthority(), PATH_COMMIT, CODE_COMMIT); | ||||
|         MATCHER.addURI(getAuthority(), PATH_INIT + "/#", CODE_INIT); | ||||
|         MATCHER.addURI(getAuthority(), PATH_COMMIT + "/#", CODE_COMMIT); | ||||
|         MATCHER.addURI(getAuthority(), PATH_APPS + "/#/*", APPS); | ||||
|         MATCHER.addURI(getAuthority(), PATH_SPECIFIC_APP + "/#/*", CODE_SINGLE); | ||||
|     } | ||||
| @ -67,19 +66,6 @@ public class TempAppProvider extends AppProvider { | ||||
|         return Uri.parse("content://" + getAuthority()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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<String> apps, long repoId) { | ||||
|         return getContentUri().buildUpon() | ||||
|                 .appendPath(PATH_APPS) | ||||
| @ -105,10 +91,13 @@ public class TempAppProvider extends AppProvider { | ||||
|          * Deletes the old temporary table (if it exists). Then creates a new temporary apk provider | ||||
|          * table and populates it with all the data from the real apk provider table. | ||||
|          */ | ||||
|         public static void init(Context context) { | ||||
|             Uri uri = Uri.withAppendedPath(getContentUri(), PATH_INIT); | ||||
|         public static void init(Context context, long repoIdToUpdate) { | ||||
|             Uri uri = getContentUri().buildUpon() | ||||
|                     .appendPath(PATH_INIT) | ||||
|                     .appendPath(Long.toString(repoIdToUpdate)) | ||||
|                     .build(); | ||||
|             context.getContentResolver().insert(uri, new ContentValues()); | ||||
|             TempApkProvider.Helper.init(context); | ||||
|             TempApkProvider.Helper.init(context, repoIdToUpdate); | ||||
|         } | ||||
| 
 | ||||
|         public static List<App> findByPackageNames(Context context, | ||||
| @ -122,8 +111,11 @@ public class TempAppProvider extends AppProvider { | ||||
|          * Saves data from the temp table to the apk table, by removing _EVERYTHING_ from the real | ||||
|          * apk table and inserting all of the records from here. The temporary table is then removed. | ||||
|          */ | ||||
|         public static void commitAppsAndApks(Context context) { | ||||
|             Uri uri = Uri.withAppendedPath(getContentUri(), PATH_COMMIT); | ||||
|         public static void commitAppsAndApks(Context context, long repoIdToCommit) { | ||||
|             Uri uri = getContentUri().buildUpon() | ||||
|                     .appendPath(PATH_COMMIT) | ||||
|                     .appendPath(Long.toString(repoIdToCommit)) | ||||
|                     .build(); | ||||
|             context.getContentResolver().insert(uri, new ContentValues()); | ||||
|         } | ||||
|     } | ||||
| @ -137,11 +129,11 @@ public class TempAppProvider extends AppProvider { | ||||
|     public Uri insert(Uri uri, ContentValues values) { | ||||
|         switch (MATCHER.match(uri)) { | ||||
|             case CODE_INIT: | ||||
|                 initTable(); | ||||
|                 initTable(Long.parseLong(uri.getLastPathSegment())); | ||||
|                 return null; | ||||
|             case CODE_COMMIT: | ||||
|                 updateAllAppDetails(); | ||||
|                 commitTable(); | ||||
|                 commitTable(Long.parseLong(uri.getLastPathSegment())); | ||||
|                 return null; | ||||
|             default: | ||||
|                 return super.insert(uri, values); | ||||
| @ -150,47 +142,7 @@ public class TempAppProvider extends AppProvider { | ||||
| 
 | ||||
|     @Override | ||||
|     public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { | ||||
|         if (MATCHER.match(uri) != CODE_SINGLE) { | ||||
|             throw new UnsupportedOperationException("Update not supported for " + uri + "."); | ||||
|         } | ||||
| 
 | ||||
|         if (values.containsKey(Cols.DESCRIPTION) && values.getAsString(Cols.DESCRIPTION) == null) { | ||||
|             // the database does not let a description be set as null | ||||
|             values.put(Cols.DESCRIPTION, ""); | ||||
|         } | ||||
| 
 | ||||
|         List<String> 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); | ||||
| 
 | ||||
|         if (values.containsKey(Cols.ForWriting.Categories.CATEGORIES)) { | ||||
|             String[] categories = Utils.parseCommaSeparatedString( | ||||
|                     values.getAsString(Cols.ForWriting.Categories.CATEGORIES)); | ||||
|             ensureCategories(categories, packageName, repoId); | ||||
|             values.remove(Cols.ForWriting.Categories.CATEGORIES); | ||||
|         } | ||||
| 
 | ||||
|         int count = db().update(getTableName(), values, query.getSelection(), query.getArgs()); | ||||
|         if (!isApplyingBatch()) { | ||||
|             getContext().getContentResolver().notifyChange(getHighestPriorityMetadataUri(packageName), null); | ||||
|         } | ||||
|         return count; | ||||
|     } | ||||
| 
 | ||||
|     private void ensureCategories(String[] categories, String packageName, long repoId) { | ||||
|         Query query = new AppProvider.Query(); | ||||
|         query.addField(Cols.ROW_ID); | ||||
|         query.addSelection(querySingle(packageName, repoId)); | ||||
|         Cursor cursor = db().rawQuery(query.toString(), query.getArgs()); | ||||
|         cursor.moveToFirst(); | ||||
|         long appMetadataId = cursor.getLong(0); | ||||
|         cursor.close(); | ||||
| 
 | ||||
|         ensureCategories(categories, appMetadataId); | ||||
|         throw new UnsupportedOperationException("Update not supported for " + uri + "."); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @ -220,29 +172,45 @@ public class TempAppProvider extends AppProvider { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void initTable() { | ||||
|     private void initTable(long repoIdBeingUpdated) { | ||||
|         final SQLiteDatabase db = db(); | ||||
| 
 | ||||
|         String mainApp = AppMetadataTable.NAME; | ||||
|         String tempApp = DB + "." + getTableName(); | ||||
|         String mainCat = CatJoinTable.NAME; | ||||
|         String tempCat = DB + "." + getCatJoinTableName(); | ||||
| 
 | ||||
|         ensureTempTableDetached(db); | ||||
|         db.execSQL("ATTACH DATABASE ':memory:' AS " + DB); | ||||
|         db.execSQL(DBHelper.CREATE_TABLE_APP_METADATA.replaceFirst(AppMetadataTable.NAME, DB + "." + getTableName())); | ||||
|         db.execSQL(DBHelper.CREATE_TABLE_CAT_JOIN.replaceFirst(CatJoinTable.NAME, DB + "." + getCatJoinTableName())); | ||||
|         db.execSQL(copyData(AppMetadataTable.Cols.ALL_COLS, AppMetadataTable.NAME, DB + "." + getTableName())); | ||||
|         db.execSQL(copyData(CatJoinTable.Cols.ALL_COLS, CatJoinTable.NAME, DB + "." + getCatJoinTableName())); | ||||
|         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 + ");"); | ||||
|         db.execSQL(DBHelper.CREATE_TABLE_APP_METADATA.replaceFirst(AppMetadataTable.NAME, tempApp)); | ||||
|         db.execSQL(DBHelper.CREATE_TABLE_CAT_JOIN.replaceFirst(CatJoinTable.NAME, tempCat)); | ||||
| 
 | ||||
|         String appWhere = mainApp + "." + Cols.REPO_ID + " != ?"; | ||||
|         String[] repoArgs = new String[]{Long.toString(repoIdBeingUpdated)}; | ||||
|         db.execSQL(copyData(Cols.ALL_COLS, mainApp, tempApp, appWhere), repoArgs); | ||||
| 
 | ||||
|         // TODO: String catWhere = mainCat + "." + CatJoinTable.Cols..Cols.REPO_ID + " != ?"; | ||||
|         db.execSQL(copyData(CatJoinTable.Cols.ALL_COLS, mainCat, tempCat, null)); | ||||
| 
 | ||||
|         db.execSQL("CREATE INDEX IF NOT EXISTS " + DB + ".app_id ON " + getTableName() + " (" + Cols.PACKAGE_ID + ");"); | ||||
|         db.execSQL("CREATE INDEX IF NOT EXISTS " + DB + ".app_upstreamVercode ON " + getTableName() + " (" + Cols.UPSTREAM_VERSION_CODE + ");"); | ||||
|         db.execSQL("CREATE INDEX IF NOT EXISTS " + DB + ".app_compatible ON " + getTableName() + " (" + Cols.IS_COMPATIBLE + ");"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Constructs an INSERT INTO ... SELECT statement as a means from getting data from one table | ||||
|      * into another. The list of columns to copy are explicitly specified using colsToCopy. | ||||
|      */ | ||||
|     static String copyData(String[] colsToCopy, String fromTable, String toTable) { | ||||
|     static String copyData(String[] colsToCopy, String fromTable, String toTable, String where) { | ||||
|         String cols = TextUtils.join(", ", colsToCopy); | ||||
|         return "INSERT INTO " + toTable + " (" + cols + ") SELECT " + cols + " FROM " + fromTable; | ||||
|         String sql = "INSERT INTO " + toTable + " (" + cols + ") SELECT " + cols + " FROM " + fromTable; | ||||
|         if (!TextUtils.isEmpty(where)) { | ||||
|             sql += " WHERE " + where; | ||||
|         } | ||||
|         return sql; | ||||
|     } | ||||
| 
 | ||||
|     private void commitTable() { | ||||
|     private void commitTable(long repoIdToCommit) { | ||||
|         final SQLiteDatabase db = db(); | ||||
|         try { | ||||
|             db.beginTransaction(); | ||||
| @ -251,14 +219,16 @@ public class TempAppProvider extends AppProvider { | ||||
|             final String tempApk = DB + "." + TempApkProvider.TABLE_TEMP_APK; | ||||
|             final String tempCatJoin = DB + "." + TABLE_TEMP_CAT_JOIN; | ||||
| 
 | ||||
|             db.execSQL("DELETE FROM " + AppMetadataTable.NAME + " WHERE 1"); | ||||
|             db.execSQL(copyData(AppMetadataTable.Cols.ALL_COLS, tempApp, AppMetadataTable.NAME)); | ||||
|             final String[] repoArgs = new String[]{Long.toString(repoIdToCommit)}; | ||||
| 
 | ||||
|             db.execSQL("DELETE FROM " + ApkTable.NAME + " WHERE 1"); | ||||
|             db.execSQL(copyData(ApkTable.Cols.ALL_COLS, tempApk, ApkTable.NAME)); | ||||
|             db.execSQL("DELETE FROM " + AppMetadataTable.NAME + " WHERE " + Cols.REPO_ID + " = ?", repoArgs); | ||||
|             db.execSQL(copyData(Cols.ALL_COLS, tempApp, AppMetadataTable.NAME, Cols.REPO_ID + " = ?"), repoArgs); | ||||
| 
 | ||||
|             db.execSQL("DELETE FROM " + CatJoinTable.NAME + " WHERE 1"); | ||||
|             db.execSQL(copyData(CatJoinTable.Cols.ALL_COLS, tempCatJoin, CatJoinTable.NAME)); | ||||
|             db.execSQL("DELETE FROM " + ApkTable.NAME + " WHERE " + ApkTable.Cols.REPO_ID + " = ?", repoArgs); | ||||
|             db.execSQL(copyData(ApkTable.Cols.ALL_COLS, tempApk, ApkTable.NAME, ApkTable.Cols.REPO_ID + " = ?"), repoArgs); | ||||
| 
 | ||||
|             db.execSQL("DELETE FROM " + CatJoinTable.NAME + " WHERE " + getCatRepoWhere(CatJoinTable.NAME), repoArgs); | ||||
|             db.execSQL(copyData(CatJoinTable.Cols.ALL_COLS, tempCatJoin, CatJoinTable.NAME, getCatRepoWhere(tempCatJoin)), repoArgs); | ||||
| 
 | ||||
|             db.setTransactionSuccessful(); | ||||
| 
 | ||||
| @ -270,4 +240,14 @@ public class TempAppProvider extends AppProvider { | ||||
|             db.execSQL("DETACH DATABASE " + DB); // Can't be done in a transaction. | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private String getCatRepoWhere(String categoryTable) { | ||||
|         String catRepoSubquery = | ||||
|                 "SELECT DISTINCT innerCatJoin." + CatJoinTable.Cols.ROW_ID + " " + | ||||
|                 "FROM " + categoryTable + " AS innerCatJoin " + | ||||
|                 "JOIN " + getTableName() + " AS app ON (app." + Cols.ROW_ID + " = innerCatJoin." + CatJoinTable.Cols.APP_METADATA_ID + ") " + | ||||
|                 "WHERE app." + Cols.REPO_ID + " = ?"; | ||||
| 
 | ||||
|         return CatJoinTable.Cols.ROW_ID + " IN (" + catRepoSubquery + ")"; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -238,69 +238,6 @@ public class ApkProviderTest extends FDroidProviderTest { | ||||
|         assertEquals(10, apk.repoId); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void testKnownApks() { | ||||
| 
 | ||||
|         App fdroid = Assert.ensureApp(context, "org.fdroid.fdroid"); | ||||
|         for (int i = 0; i < 7; i++) { | ||||
|             Assert.insertApk(context, fdroid, i); | ||||
|         } | ||||
| 
 | ||||
|         App exampleOrg = Assert.ensureApp(context, "org.example"); | ||||
|         for (int i = 0; i < 9; i++) { | ||||
|             Assert.insertApk(context, exampleOrg, i); | ||||
|         } | ||||
| 
 | ||||
|         App exampleCom = Assert.ensureApp(context, "com.example"); | ||||
|         for (int i = 0; i < 3; i++) { | ||||
|             Assert.insertApk(context, exampleCom, i); | ||||
|         } | ||||
| 
 | ||||
|         App thingo = Assert.ensureApp(context, "com.apk.thingo"); | ||||
|         Assert.insertApk(context, thingo, 1); | ||||
| 
 | ||||
|         Apk[] known = { | ||||
|             new MockApk(fdroid, 1), | ||||
|             new MockApk(fdroid, 3), | ||||
|             new MockApk(fdroid, 5), | ||||
| 
 | ||||
|             new MockApk(exampleCom, 1), | ||||
|             new MockApk(exampleCom, 2), | ||||
|         }; | ||||
| 
 | ||||
|         Apk[] unknown = { | ||||
|             new MockApk(fdroid, 7), | ||||
|             new MockApk(fdroid, 9), | ||||
|             new MockApk(fdroid, 11), | ||||
|             new MockApk(fdroid, 13), | ||||
| 
 | ||||
|             new MockApk(exampleCom, 3), | ||||
|             new MockApk(exampleCom, 4), | ||||
|             new MockApk(exampleCom, 5), | ||||
| 
 | ||||
|             new MockApk(-10, 1), | ||||
|             new MockApk(-10, 2), | ||||
|         }; | ||||
| 
 | ||||
|         List<Apk> apksToCheck = new ArrayList<>(known.length + unknown.length); | ||||
|         Collections.addAll(apksToCheck, known); | ||||
|         Collections.addAll(apksToCheck, unknown); | ||||
| 
 | ||||
|         String[] projection = { | ||||
|             Cols.Package.PACKAGE_NAME, | ||||
|             Cols.APP_ID, | ||||
|             Cols.VERSION_CODE, | ||||
|         }; | ||||
| 
 | ||||
|         List<Apk> knownApks = ApkProvider.Helper.knownApks(context, apksToCheck, projection); | ||||
| 
 | ||||
|         assertResultCount(known.length, knownApks); | ||||
| 
 | ||||
|         for (Apk knownApk : knownApks) { | ||||
|             assertContains(knownApks, knownApk); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     public void testFindByApp() { | ||||
| 
 | ||||
|  | ||||
| @ -23,6 +23,7 @@ public abstract class FDroidProviderTest { | ||||
| 
 | ||||
|     @After | ||||
|     public final void tearDownBase() { | ||||
|         CategoryProvider.Helper.clearCategoryIdCache(); | ||||
|         FDroidProvider.clearDbHelperSingleton(); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -110,7 +110,7 @@ public class ProviderUriTests { | ||||
| 
 | ||||
|         // Required so that the `assertValidUri` calls below will indeed have a real temp_fdroid_app | ||||
|         // table to query. | ||||
|         TempAppProvider.Helper.init(TestUtils.createContextWithContentResolver(resolver)); | ||||
|         TempAppProvider.Helper.init(TestUtils.createContextWithContentResolver(resolver), 123); | ||||
| 
 | ||||
|         List<String> packageNames = new ArrayList<>(2); | ||||
|         packageNames.add("org.fdroid.fdroid"); | ||||
| @ -140,29 +140,7 @@ 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-any-repo/100/org.fdroid.fdroid", projection); | ||||
|         assertValidUri(resolver, ApkProvider.getContentUri(apks), projection); | ||||
|         assertValidUri(resolver, ApkProvider.getApkFromAnyRepoUri("org.fdroid.fdroid", 100, null), "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); | ||||
|     } | ||||
| 
 | ||||
|     @Test(expected = IllegalArgumentException.class) | ||||
|     public void invalidApkUrisWithTooManyApks() { | ||||
|         String[] projection = Schema.ApkTable.Cols.ALL; | ||||
| 
 | ||||
|         List<Apk> manyApks = new ArrayList<>(ApkProvider.MAX_APKS_TO_QUERY - 5); | ||||
|         for (int i = 0; i < ApkProvider.MAX_APKS_TO_QUERY - 1; i++) { | ||||
|             manyApks.add(new MockApk("com.example." + i, i)); | ||||
|         } | ||||
|         assertValidUri(resolver, ApkProvider.getContentUri(manyApks), projection); | ||||
| 
 | ||||
|         manyApks.add(new MockApk("org.fdroid.fdroid.1", 1)); | ||||
|         manyApks.add(new MockApk("org.fdroid.fdroid.2", 2)); | ||||
| 
 | ||||
|         // Technically, it is a valid URI, because it doesn't | ||||
|         // throw an UnsupportedOperationException. However it | ||||
|         // is still not okay (we run out of bindable parameters | ||||
|         // in the sqlite query. | ||||
|         assertValidUri(resolver, ApkProvider.getContentUri(manyApks), projection); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -45,8 +45,6 @@ public class ProperMultiRepoUpdaterTest extends MultiRepoUpdaterTest { | ||||
|     @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()); | ||||
| @ -67,7 +65,7 @@ public class ProperMultiRepoUpdaterTest extends MultiRepoUpdaterTest { | ||||
|         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 { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Hans-Christoph Steiner
						Hans-Christoph Steiner