From b6218c6d056e9baaf9ae4422c3574f7e63ff7ba9 Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Tue, 3 Nov 2015 00:57:51 +1100 Subject: [PATCH 1/7] Refactored code which inserts apps into database to make testable. Putting it in the UpdateService made it a little tricky to test, so I moved it out to a separate class called `RepoPersister`. --- .../src/org/fdroid/fdroid/RepoPersister.java | 329 ++++++++++++++++++ .../src/org/fdroid/fdroid/UpdateService.java | 303 +--------------- 2 files changed, 344 insertions(+), 288 deletions(-) create mode 100644 F-Droid/src/org/fdroid/fdroid/RepoPersister.java diff --git a/F-Droid/src/org/fdroid/fdroid/RepoPersister.java b/F-Droid/src/org/fdroid/fdroid/RepoPersister.java new file mode 100644 index 000000000..3f82422be --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/RepoPersister.java @@ -0,0 +1,329 @@ +package org.fdroid.fdroid; + +import android.content.ContentProviderOperation; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.support.annotation.NonNull; +import android.util.Log; + +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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Saves app and apk information to the database after a {@link RepoUpdater} has processed the + * relevant index file. + */ +public class RepoPersister { + + private static final String TAG = "RepoPersister"; + + /** + * When an app already exists in the db, and we are updating it on the off chance that some + * values changed in the index, some fields should not be updated. Rather, they should be + * ignored, because they were explicitly set by the user, and hence can't be automatically + * overridden by the index. + * + * NOTE: In the future, these attributes will be moved to a join table, so that the app table + * is essentially completely transient, and can be nuked at any time. + */ + private static final String[] APP_FIELDS_TO_IGNORE = { + AppProvider.DataColumns.IGNORE_ALLUPDATES, + AppProvider.DataColumns.IGNORE_THISUPDATE, + }; + + @NonNull + private final Context context; + + private Map<String, App> appsToUpdate = new HashMap<>(); + private List<Apk> apksToUpdate = new ArrayList<>(); + private List<Repo> repos = new ArrayList<>(); + + public RepoPersister(@NonNull Context context) { + this.context = context; + } + + public RepoPersister queueUpdater(RepoUpdater updater) { + queueApps(updater.getApps()); + queueApks(updater.getApks()); + repos.add(updater.repo); + return this; + } + + private void queueApps(List<App> apps) { + for (final App app : apps) { + appsToUpdate.put(app.id, app); + } + } + + private void queueApks(List<Apk> apks) { + apksToUpdate.addAll(apks); + } + + public void save(List<Repo> disabledRepos) { + + List<App> listOfAppsToUpdate = new ArrayList<>(); + listOfAppsToUpdate.addAll(appsToUpdate.values()); + + calcApkCompatibilityFlags(apksToUpdate); + + // Need to do this BEFORE updating the apks, otherwise when it continually + // calls "get existing apks for repo X" then it will be getting the newly + // created apks, rather than those from the fresh, juicy index we just processed. + removeApksNoLongerInRepo(apksToUpdate, repos); + + int totalInsertsUpdates = listOfAppsToUpdate.size() + apksToUpdate.size(); + updateOrInsertApps(listOfAppsToUpdate, totalInsertsUpdates, 0); + updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size()); + removeApksFromRepos(disabledRepos); + removeAppsWithoutApks(); + + // This will sort out the icon urls, compatibility flags. and suggested version + // for each app. It used to happen here in Java code, but was moved to SQL when + // it became apparant we don't always have enough info (depending on which repos + // were updated). + AppProvider.Helper.calcDetailsFromIndex(context); + + } + + /** + * 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). + * The reason is that we need to interact with the CompatibilityChecker + * in order to see if, and why an apk is not compatible. + */ + private void calcApkCompatibilityFlags(List<Apk> apks) { + final CompatibilityChecker checker = new CompatibilityChecker(context); + for (final Apk apk : apks) { + final List<String> reasons = checker.getIncompatibleReasons(apk); + if (reasons.size() > 0) { + apk.compatible = false; + apk.incompatibleReasons = Utils.CommaSeparatedList.make(reasons); + } else { + apk.compatible = true; + apk.incompatibleReasons = null; + } + } + } + + /** + * If a repo was updated (i.e. it is in use, and the index has changed + * since last time we did an update), then we want to remove any apks that + * belong to the repo which are not in the current list of apks that were + * retrieved. + */ + private void removeApksNoLongerInRepo(List<Apk> apksToUpdate, List<Repo> updatedRepos) { + + long startTime = System.currentTimeMillis(); + List<Apk> toRemove = new ArrayList<>(); + + final String[] fields = { + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.VERSION_CODE, + ApkProvider.DataColumns.VERSION, + }; + + for (final Repo repo : updatedRepos) { + final List<Apk> existingApks = ApkProvider.Helper.findByRepo(context, repo, fields); + for (final Apk existingApk : existingApks) { + if (!isApkToBeUpdated(existingApk, apksToUpdate)) { + toRemove.add(existingApk); + } + } + } + + long duration = System.currentTimeMillis() - startTime; + Utils.debugLog(TAG, "Found " + toRemove.size() + " apks no longer in the updated repos (took " + duration + "ms)"); + + if (toRemove.size() > 0) { + ApkProvider.Helper.deleteApks(context, toRemove); + } + } + + private void updateOrInsertApps(List<App> appsToUpdate, int totalUpdateCount, int currentCount) { + + List<ContentProviderOperation> operations = new ArrayList<>(); + List<String> knownAppIds = getKnownAppIds(appsToUpdate); + for (final App app : appsToUpdate) { + if (knownAppIds.contains(app.id)) { + operations.add(updateExistingApp(app)); + } else { + operations.add(insertNewApp(app)); + } + } + + Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apps."); + try { + executeBatchWithStatus(AppProvider.getAuthority(), operations, currentCount, totalUpdateCount); + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Could not update or insert apps", e); + } + } + + private void executeBatchWithStatus(String providerAuthority, + List<ContentProviderOperation> operations, + int currentCount, + int totalUpdateCount) + throws RemoteException, OperationApplicationException { + int i = 0; + while (i < operations.size()) { + int count = Math.min(operations.size() - i, 100); + ArrayList<ContentProviderOperation> o = new ArrayList<>(operations.subList(i, i + count)); + UpdateService.sendStatus(context, UpdateService.STATUS_INFO, context.getString( + R.string.status_inserting, + (int) ((double) (currentCount + i) / totalUpdateCount * 100))); + context.getContentResolver().applyBatch(providerAuthority, o); + i += 100; + } + } + + /** + * Return list of apps from the "apks" argument which are already in the database. + */ + private List<Apk> getKnownApks(List<Apk> apks) { + final String[] fields = { + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.VERSION, + ApkProvider.DataColumns.VERSION_CODE, + }; + return ApkProvider.Helper.knownApks(context, apks, fields); + } + + private void updateOrInsertApks(List<Apk> apksToUpdate, int totalApksAppsCount, int currentCount) { + + List<ContentProviderOperation> operations = new ArrayList<>(); + + List<Apk> knownApks = getKnownApks(apksToUpdate); + for (final Apk apk : apksToUpdate) { + boolean known = false; + for (final Apk knownApk : knownApks) { + if (knownApk.id.equals(apk.id) && knownApk.vercode == apk.vercode) { + known = true; + break; + } + } + + if (known) { + operations.add(updateExistingApk(apk)); + } else { + operations.add(insertNewApk(apk)); + knownApks.add(apk); // In case another repo has the same version/id combo for this apk. + } + } + + Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apks."); + try { + executeBatchWithStatus(ApkProvider.getAuthority(), operations, currentCount, totalApksAppsCount); + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Could not update/insert apps", e); + } + } + + private ContentProviderOperation updateExistingApk(final Apk apk) { + Uri uri = ApkProvider.getContentUri(apk); + ContentValues values = apk.toContentValues(); + return ContentProviderOperation.newUpdate(uri).withValues(values).build(); + } + + private ContentProviderOperation insertNewApk(final Apk apk) { + ContentValues values = apk.toContentValues(); + Uri uri = ApkProvider.getContentUri(); + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + private ContentProviderOperation updateExistingApp(App app) { + Uri uri = AppProvider.getContentUri(app); + ContentValues values = app.toContentValues(); + for (final String toIgnore : APP_FIELDS_TO_IGNORE) { + if (values.containsKey(toIgnore)) { + values.remove(toIgnore); + } + } + return ContentProviderOperation.newUpdate(uri).withValues(values).build(); + } + + private ContentProviderOperation insertNewApp(App app) { + ContentValues values = app.toContentValues(); + Uri uri = AppProvider.getContentUri(); + return ContentProviderOperation.newInsert(uri).withValues(values).build(); + } + + private static boolean isApkToBeUpdated(Apk existingApk, List<Apk> apksToUpdate) { + for (final Apk apkToUpdate : apksToUpdate) { + if (apkToUpdate.vercode == existingApk.vercode && apkToUpdate.id.equals(existingApk.id)) { + return true; + } + } + return false; + } + + private void removeApksFromRepos(List<Repo> repos) { + for (final Repo repo : repos) { + Uri uri = ApkProvider.getRepoUri(repo.getId()); + int numDeleted = context.getContentResolver().delete(uri, null, null); + Utils.debugLog(TAG, "Removing " + numDeleted + " apks from repo " + repo.address); + } + } + + private void removeAppsWithoutApks() { + int numDeleted = context.getContentResolver().delete(AppProvider.getNoApksUri(), null, null); + Utils.debugLog(TAG, "Removing " + numDeleted + " apks that don't have any apks"); + } + + private List<String> getKnownAppIds(List<App> apps) { + List<String> knownAppIds = new ArrayList<>(); + if (apps.isEmpty()) { + return knownAppIds; + } + if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) { + int middle = apps.size() / 2; + List<App> apps1 = apps.subList(0, middle); + List<App> apps2 = apps.subList(middle, apps.size()); + knownAppIds.addAll(getKnownAppIds(apps1)); + knownAppIds.addAll(getKnownAppIds(apps2)); + } else { + knownAppIds.addAll(getKnownAppIdsFromProvider(apps)); + } + return knownAppIds; + } + + /** + * 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 List<String> getKnownAppIdsFromProvider(List<App> apps) { + + final Uri uri = AppProvider.getContentUri(apps); + final String[] fields = {AppProvider.DataColumns.APP_ID}; + Cursor cursor = context.getContentResolver().query(uri, fields, null, null, null); + + int knownIdCount = cursor != null ? cursor.getCount() : 0; + List<String> knownIds = new ArrayList<>(knownIdCount); + if (cursor != null) { + if (knownIdCount > 0) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + knownIds.add(cursor.getString(0)); + cursor.moveToNext(); + } + } + cursor.close(); + } + + return knownIds; + } + +} + diff --git a/F-Droid/src/org/fdroid/fdroid/UpdateService.java b/F-Droid/src/org/fdroid/fdroid/UpdateService.java index 06872e5c7..9556523cd 100644 --- a/F-Droid/src/org/fdroid/fdroid/UpdateService.java +++ b/F-Droid/src/org/fdroid/fdroid/UpdateService.java @@ -23,19 +23,14 @@ import android.app.IntentService; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; -import android.content.ContentProviderOperation; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.OperationApplicationException; import android.content.SharedPreferences; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.net.Uri; import android.os.Build; -import android.os.RemoteException; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; @@ -45,7 +40,6 @@ import android.text.TextUtils; import android.util.Log; import android.widget.Toast; -import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.ApkProvider; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.AppProvider; @@ -54,9 +48,7 @@ import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.net.Downloader; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; public class UpdateService extends IntentService implements ProgressListener { @@ -89,20 +81,6 @@ public class UpdateService extends IntentService implements ProgressListener { super("UpdateService"); } - /** - * When an app already exists in the db, and we are updating it on the off chance that some - * values changed in the index, some fields should not be updated. Rather, they should be - * ignored, because they were explicitly set by the user, and hence can't be automatically - * overridden by the index. - * - * NOTE: In the future, these attributes will be moved to a join table, so that the app table - * is essentially completely transient, and can be nuked at any time. - */ - private static final String[] APP_FIELDS_TO_IGNORE = { - AppProvider.DataColumns.IGNORE_ALLUPDATES, - AppProvider.DataColumns.IGNORE_THISUPDATE, - }; - public static void updateNow(Context context) { updateRepoNow(null, context); } @@ -178,16 +156,16 @@ public class UpdateService extends IntentService implements ProgressListener { localBroadcastManager.unregisterReceiver(updateStatusReceiver); } - protected void sendStatus(int statusCode) { - sendStatus(statusCode, null); + protected static void sendStatus(Context context, int statusCode) { + sendStatus(context, statusCode, null); } - protected void sendStatus(int statusCode, String message) { + protected static void sendStatus(Context context, int statusCode, String message) { Intent intent = new Intent(LOCAL_ACTION_STATUS); intent.putExtra(EXTRA_STATUS_CODE, statusCode); if (!TextUtils.isEmpty(message)) intent.putExtra(EXTRA_MESSAGE, message); - LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); } protected void sendRepoErrorStatus(int statusCode, ArrayList<CharSequence> repoErrors) { @@ -219,7 +197,7 @@ public class UpdateService extends IntentService implements ProgressListener { String totalSizeFriendly = Utils.getFriendlySize(totalSize); message = getString(R.string.status_download, repoAddress, downloadedSizeFriendly, totalSizeFriendly, percent); } - sendStatus(STATUS_INFO, message); + sendStatus(context, STATUS_INFO, message); } }; @@ -354,8 +332,8 @@ public class UpdateService extends IntentService implements ProgressListener { List<Repo> repos = RepoProvider.Helper.all(this); // Process each repo... - Map<String, App> appsToUpdate = new HashMap<>(); - List<Apk> apksToUpdate = new ArrayList<>(); + RepoPersister appSaver = new RepoPersister(this); + //List<Repo> swapRepos = new ArrayList<>(); List<Repo> unchangedRepos = new ArrayList<>(); List<Repo> updatedRepos = new ArrayList<>(); @@ -380,16 +358,13 @@ public class UpdateService extends IntentService implements ProgressListener { continue; } - sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); + sendStatus(this, STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address)); RepoUpdater updater = new RepoUpdater(getBaseContext(), repo); updater.setProgressListener(this); try { updater.update(); if (updater.hasChanged()) { - for (final App app : updater.getApps()) { - appsToUpdate.put(app.id, app); - } - apksToUpdate.addAll(updater.getApks()); + appSaver.queueUpdater(updater); updatedRepos.add(repo); changes = true; repoUpdateRememberers.add(updater.getRememberer()); @@ -406,29 +381,9 @@ public class UpdateService extends IntentService implements ProgressListener { if (!changes) { Utils.debugLog(TAG, "Not checking app details or compatibility, because all repos were up to date."); } else { - sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility)); + sendStatus(this, STATUS_INFO, getString(R.string.status_checking_compatibility)); - List<App> listOfAppsToUpdate = new ArrayList<>(); - listOfAppsToUpdate.addAll(appsToUpdate.values()); - - calcApkCompatibilityFlags(this, apksToUpdate); - - // Need to do this BEFORE updating the apks, otherwise when it continually - // calls "get existing apks for repo X" then it will be getting the newly - // created apks, rather than those from the fresh, juicy index we just processed. - removeApksNoLongerInRepo(apksToUpdate, updatedRepos); - - int totalInsertsUpdates = listOfAppsToUpdate.size() + apksToUpdate.size(); - updateOrInsertApps(listOfAppsToUpdate, totalInsertsUpdates, 0); - updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size()); - removeApksFromRepos(disabledRepos); - removeAppsWithoutApks(); - - // This will sort out the icon urls, compatibility flags. and suggested version - // for each app. It used to happen here in Java code, but was moved to SQL when - // it became apparant we don't always have enough info (depending on which repos - // were updated). - AppProvider.Helper.calcDetailsFromIndex(this); + appSaver.save(disabledRepos); notifyContentProviders(); @@ -448,9 +403,9 @@ public class UpdateService extends IntentService implements ProgressListener { if (errorRepos.isEmpty()) { if (changes) { - sendStatus(STATUS_COMPLETE_WITH_CHANGES); + sendStatus(this, STATUS_COMPLETE_WITH_CHANGES); } else { - sendStatus(STATUS_COMPLETE_AND_SAME); + sendStatus(this, STATUS_COMPLETE_AND_SAME); } } else { if (updatedRepos.size() + unchangedRepos.size() == 0) { @@ -461,7 +416,7 @@ public class UpdateService extends IntentService implements ProgressListener { } } catch (Exception e) { Log.e(TAG, "Exception during update processing", e); - sendStatus(STATUS_ERROR_GLOBAL, e.getMessage()); + sendStatus(this, STATUS_ERROR_GLOBAL, e.getMessage()); } } @@ -470,26 +425,6 @@ public class UpdateService extends IntentService implements ProgressListener { getContentResolver().notifyChange(ApkProvider.getContentUri(), null); } - /** - * 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). - * The reason is that we need to interact with the CompatibilityChecker - * in order to see if, and why an apk is not compatible. - */ - private static void calcApkCompatibilityFlags(Context context, List<Apk> apks) { - final CompatibilityChecker checker = new CompatibilityChecker(context); - for (final Apk apk : apks) { - final List<String> reasons = checker.getIncompatibleReasons(apk); - if (reasons.size() > 0) { - apk.compatible = false; - apk.incompatibleReasons = Utils.CommaSeparatedList.make(reasons); - } else { - apk.compatible = true; - apk.incompatibleReasons = null; - } - } - } - private void performUpdateNotification() { Cursor cursor = getContentResolver().query( AppProvider.getCanUpdateUri(), @@ -557,214 +492,6 @@ public class UpdateService extends IntentService implements ProgressListener { notificationManager.notify(NOTIFY_ID_UPDATES_AVAILABLE, builder.build()); } - private List<String> getKnownAppIds(List<App> apps) { - List<String> knownAppIds = new ArrayList<>(); - if (apps.isEmpty()) { - return knownAppIds; - } - if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) { - int middle = apps.size() / 2; - List<App> apps1 = apps.subList(0, middle); - List<App> apps2 = apps.subList(middle, apps.size()); - knownAppIds.addAll(getKnownAppIds(apps1)); - knownAppIds.addAll(getKnownAppIds(apps2)); - } else { - knownAppIds.addAll(getKnownAppIdsFromProvider(apps)); - } - return knownAppIds; - } - - /** - * 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 List<String> getKnownAppIdsFromProvider(List<App> apps) { - - final Uri uri = AppProvider.getContentUri(apps); - final String[] fields = {AppProvider.DataColumns.APP_ID}; - Cursor cursor = getContentResolver().query(uri, fields, null, null, null); - - int knownIdCount = cursor != null ? cursor.getCount() : 0; - List<String> knownIds = new ArrayList<>(knownIdCount); - if (cursor != null) { - if (knownIdCount > 0) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - knownIds.add(cursor.getString(0)); - cursor.moveToNext(); - } - } - cursor.close(); - } - - return knownIds; - } - - private void updateOrInsertApps(List<App> appsToUpdate, int totalUpdateCount, int currentCount) { - - List<ContentProviderOperation> operations = new ArrayList<>(); - List<String> knownAppIds = getKnownAppIds(appsToUpdate); - for (final App app : appsToUpdate) { - if (knownAppIds.contains(app.id)) { - operations.add(updateExistingApp(app)); - } else { - operations.add(insertNewApp(app)); - } - } - - Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apps."); - try { - executeBatchWithStatus(AppProvider.getAuthority(), operations, currentCount, totalUpdateCount); - } catch (RemoteException | OperationApplicationException e) { - Log.e(TAG, "Could not update or insert apps", e); - } - } - - private void executeBatchWithStatus(String providerAuthority, - List<ContentProviderOperation> operations, - int currentCount, - int totalUpdateCount) - throws RemoteException, OperationApplicationException { - int i = 0; - while (i < operations.size()) { - int count = Math.min(operations.size() - i, 100); - ArrayList<ContentProviderOperation> o = new ArrayList<>(operations.subList(i, i + count)); - sendStatus(STATUS_INFO, getString( - R.string.status_inserting, - (int) ((double) (currentCount + i) / totalUpdateCount * 100))); - getContentResolver().applyBatch(providerAuthority, o); - i += 100; - } - } - - /** - * Return list of apps from the "apks" argument which are already in the database. - */ - private List<Apk> getKnownApks(List<Apk> apks) { - final String[] fields = { - ApkProvider.DataColumns.APK_ID, - ApkProvider.DataColumns.VERSION, - ApkProvider.DataColumns.VERSION_CODE, - }; - return ApkProvider.Helper.knownApks(this, apks, fields); - } - - private void updateOrInsertApks(List<Apk> apksToUpdate, int totalApksAppsCount, int currentCount) { - - List<ContentProviderOperation> operations = new ArrayList<>(); - - List<Apk> knownApks = getKnownApks(apksToUpdate); - for (final Apk apk : apksToUpdate) { - boolean known = false; - for (final Apk knownApk : knownApks) { - if (knownApk.id.equals(apk.id) && knownApk.vercode == apk.vercode) { - known = true; - break; - } - } - - if (known) { - operations.add(updateExistingApk(apk)); - } else { - operations.add(insertNewApk(apk)); - knownApks.add(apk); // In case another repo has the same version/id combo for this apk. - } - } - - Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apks."); - try { - executeBatchWithStatus(ApkProvider.getAuthority(), operations, currentCount, totalApksAppsCount); - } catch (RemoteException | OperationApplicationException e) { - Log.e(TAG, "Could not update/insert apps", e); - } - } - - private ContentProviderOperation updateExistingApk(final Apk apk) { - Uri uri = ApkProvider.getContentUri(apk); - ContentValues values = apk.toContentValues(); - return ContentProviderOperation.newUpdate(uri).withValues(values).build(); - } - - private ContentProviderOperation insertNewApk(final Apk apk) { - ContentValues values = apk.toContentValues(); - Uri uri = ApkProvider.getContentUri(); - return ContentProviderOperation.newInsert(uri).withValues(values).build(); - } - - private ContentProviderOperation updateExistingApp(App app) { - Uri uri = AppProvider.getContentUri(app); - ContentValues values = app.toContentValues(); - for (final String toIgnore : APP_FIELDS_TO_IGNORE) { - if (values.containsKey(toIgnore)) { - values.remove(toIgnore); - } - } - return ContentProviderOperation.newUpdate(uri).withValues(values).build(); - } - - private ContentProviderOperation insertNewApp(App app) { - ContentValues values = app.toContentValues(); - Uri uri = AppProvider.getContentUri(); - return ContentProviderOperation.newInsert(uri).withValues(values).build(); - } - - /** - * If a repo was updated (i.e. it is in use, and the index has changed - * since last time we did an update), then we want to remove any apks that - * belong to the repo which are not in the current list of apks that were - * retrieved. - */ - private void removeApksNoLongerInRepo(List<Apk> apksToUpdate, List<Repo> updatedRepos) { - - long startTime = System.currentTimeMillis(); - List<Apk> toRemove = new ArrayList<>(); - - final String[] fields = { - ApkProvider.DataColumns.APK_ID, - ApkProvider.DataColumns.VERSION_CODE, - ApkProvider.DataColumns.VERSION, - }; - - for (final Repo repo : updatedRepos) { - final List<Apk> existingApks = ApkProvider.Helper.findByRepo(this, repo, fields); - for (final Apk existingApk : existingApks) { - if (!isApkToBeUpdated(existingApk, apksToUpdate)) { - toRemove.add(existingApk); - } - } - } - - long duration = System.currentTimeMillis() - startTime; - Utils.debugLog(TAG, "Found " + toRemove.size() + " apks no longer in the updated repos (took " + duration + "ms)"); - - if (toRemove.size() > 0) { - ApkProvider.Helper.deleteApks(this, toRemove); - } - } - - private static boolean isApkToBeUpdated(Apk existingApk, List<Apk> apksToUpdate) { - for (final Apk apkToUpdate : apksToUpdate) { - if (apkToUpdate.vercode == existingApk.vercode && apkToUpdate.id.equals(existingApk.id)) { - return true; - } - } - return false; - } - - private void removeApksFromRepos(List<Repo> repos) { - for (final Repo repo : repos) { - Uri uri = ApkProvider.getRepoUri(repo.getId()); - int numDeleted = getContentResolver().delete(uri, null, null); - Utils.debugLog(TAG, "Removing " + numDeleted + " apks from repo " + repo.address); - } - } - - private void removeAppsWithoutApks() { - int numDeleted = getContentResolver().delete(AppProvider.getNoApksUri(), null, null); - Utils.debugLog(TAG, "Removing " + numDeleted + " apks that don't have any apks"); - } - /** * Received progress event from the RepoXMLHandler. It could be progress * downloading from the repo, or perhaps processing the info from the repo. @@ -783,6 +510,6 @@ public class UpdateService extends IntentService implements ProgressListener { message = getString(R.string.status_processing_xml_percent, repoAddress, downloadedSize, totalSize, percent); break; } - sendStatus(STATUS_INFO, message); + sendStatus(this, STATUS_INFO, message); } } From 8d1e20b7fda804a1b64fa24b3a8bc390786b9c1a Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Sun, 13 Sep 2015 11:33:53 +1000 Subject: [PATCH 2/7] Remove TargetApi(8) because that is our min-sdk now. --- F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java index 1c5a95999..a588df536 100644 --- a/F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java +++ b/F-Droid/test/src/org/fdroid/fdroid/RepoUpdaterTest.java @@ -1,7 +1,6 @@ package org.fdroid.fdroid; -import android.annotation.TargetApi; import android.content.Context; import android.test.InstrumentationTestCase; @@ -11,7 +10,6 @@ import org.fdroid.fdroid.data.Repo; import java.io.File; import java.util.UUID; -@TargetApi(8) public class RepoUpdaterTest extends InstrumentationTestCase { private static final String TAG = "RepoUpdaterTest"; From f794d1e7a52809fb55679e0f76a0f481b9f32a3b Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Sun, 13 Sep 2015 11:52:07 +1000 Subject: [PATCH 3/7] Infrastructure for doing test driven development to support multiple repo dev. The new test skeletons right now update three different repos in different configurations. They do so such that the order of updates changes and therefore the way in which conflicts between repos are dealt with are tested. They should all have the same result (though I'm not sure exactly what that should be yet). --- F-Droid/test/assets/README.md | 50 +++++++ F-Droid/test/assets/multiRepo.archive.jar | Bin 0 -> 5971 bytes F-Droid/test/assets/multiRepo.conflicting.jar | Bin 0 -> 5705 bytes F-Droid/test/assets/multiRepo.normal.jar | Bin 0 -> 5809 bytes .../fdroid/fdroid/MultiRepoUpdaterTest.java | 136 ++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 100644 F-Droid/test/assets/README.md create mode 100644 F-Droid/test/assets/multiRepo.archive.jar create mode 100644 F-Droid/test/assets/multiRepo.conflicting.jar create mode 100644 F-Droid/test/assets/multiRepo.normal.jar create mode 100644 F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java diff --git a/F-Droid/test/assets/README.md b/F-Droid/test/assets/README.md new file mode 100644 index 000000000..ea34d0a14 --- /dev/null +++ b/F-Droid/test/assets/README.md @@ -0,0 +1,50 @@ +# Multiple Repos Test + +This covers the three indexes: + * multiRepo.normal.jar + * multiRepo.archive.jar + * multiRepo.conflicting.jar + +The goal is that F-Droid client should be able to: + + * Update all three repos successfully + * Show all included versions for download in the UI + * Somehow deal nicely with the fact that two repos provide versions 50-53 of AdAway + +## multiRepo.normal.jar + + * 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) + +## multiRepo.archive.jar + + * AdAway (org.adaway) + - Version 2.9.2 (51) + - Version 2.9.1 (50) + - Version 2.9 (49) + - Version 2.8.1 (48) + - Version 2.7 (46) + - Version 2.6 (45) + - Version 2.3 (42) + - Version 2.1 (40) + - Version 1.37 (37) + - Version 1.35 (36) + - Version 1.34 (35) + +## multiRepo.conflicting.jar + + * 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) \ No newline at end of file diff --git a/F-Droid/test/assets/multiRepo.archive.jar b/F-Droid/test/assets/multiRepo.archive.jar new file mode 100644 index 0000000000000000000000000000000000000000..f5c505c073234f2bed24b4c4888d61e1d395c601 GIT binary patch literal 5971 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 AC;$Ke literal 0 HcmV?d00001 diff --git a/F-Droid/test/assets/multiRepo.conflicting.jar b/F-Droid/test/assets/multiRepo.conflicting.jar new file mode 100644 index 0000000000000000000000000000000000000000..6d26f65d3159bf7af4ce3b0c5d45a729ce5d83d2 GIT binary patch literal 5705 zcma)AWl&tpx*ZtYWsu+ufxzIBK>`E`1R30e2e$wNArLe`u)$?;m*5s$gX<uN00$>P za0vtta+6#2&dELX-mSN~de`37{jK_X?H~PpOI;a=0RrIQ-~duAxt{}m1L)_wik!AI zx1y>%uZpy)qP(1zHjj$@ca_l*m@+rdB%v}lWNdW&qbA>H!PT#D9wiknMU~0dr5Ly= zdk=aIQy{jNRIF@U1rU)fDvAv%RwN+F;RJpRzhd1idck42Sy-UEt+TQD4UXw8LbKaq zSp-DYYvSN|5EqvUMHVXwaJi`w))KIf;3$r%a@7qI)MM^o0o0YTaacWL3H*L$7x;6k z|NrurQ`b_2si~UsXvsUFq@4r#sQZtf;I8C8r_ux^mZ*4`aUmR;*mOcokmyHg^6c}v zRzFUsyk4vwG>dmWr7#k3(+-%;sk3F3&U#YKEru^~0~WCj(qikIb;1dkwMb--#uAcN z<)dXiu6}SW<ce%{irbr&;<5cUmhH%?`H4XL^6E!K^itQ>L#cLHO)VCv-hKsQ%`+7G zLo{$$=X&#XoZFy43FEDO#^H7T)oGh)Fx7IyW;t45aCwefYth{RN!^l9#w~D5P8?;` zsj51X-B7USnef5?$n=*Wy%Hx<<3Gjt@m~d@___Mm5B*CJO)crnL@$xg2$}&DwS0(q zzW_5qh-K<k6iJ?9q;Z1-XB=xS2+L+r5~&QD<xxfUOAaFIRoPoZv@KS^bYM%44Uuut z@#c%gr{md27q@;ZTes^!d;oZL$g@OPxye4mBOJW34^r6xkSzl>Mt2Vyk#56Ns2L?C z8{piOkv+S+8&h5141n2|IZlA}u8ZV!o04qxW&{|R`7H5x5s;BIM~1%sZg<cjp>4_d z+Zy2d&HL2ntH_w_*5nK0@^S8a!-wT7++?DIBeZ+_zStzvu4_pEowmSkueMtBJDLcs zWAOkz^((iVfj1mx$M?RZz1_VqG&lat?R_&m>RxTmmWQ;rArDt6d)80rocig7Bkh48 zsuxkym<DB50R^e~_FFXj93|G2;287e=oo~aq1DZwCz<a;QK`8scGS@U(3aaE#n(Ta zW!P>U<Q7bZ(>cU~D@=yeKjqcDaf(<u7PXD4#<;g7*-RCfMxU$o0*|{g_15?R*J$Is zY(=J|K9^%#Zy!j^LsxVw#>OfQY&+iUFS?V&;6Y-M1K%>p6}poruuu?7!*z`J2$-}& zAG;vkRk^GWoOp)~j5xd2nRs7fE>S*onroVH)=GsZZ)KtxUiJ`Cev6iLqZrdobaLk; zJPN<ymwuPYs^A_lGp%PWm&~4yJPPS9*jQDv$S9^LHsyRNYPK^Olz$qk^mL^O+Hds7 zO57_fXcGMzKSZPh+rg<Gs~RgFoUGXITbN`<F8g?;sK?Z<Qa%HSzJpn{t+#j>9n&Du zu5W8Vx}mfT6QdkY<K!*XNEG_a(Bhn7pJ-)Bzj(r0Ehw_M(>3LFjrOqq&)Zu)fe&QB z2Tnu$-UC0NFV(S}Eoj#FELEJ$%SIK<l9$vp_j`t}bh6(Ke%_uBnc5y7J@lJaycu(? zsKmzQB`$J(&ehiEb`>&HdrXL`P?qQ{ZIv8Wh<=`5`o~m~lNJ<@CMI}Z5Qiy3*(uXs zRkIpUm!#t0wxgcqLdxZIT4~jQK1%QsU4j(&4DCw-4{i_h4T%XS#^Q!s`3n>*ZJF1? zZ67zYiP7<X)@^g8m0Y?8X@I6{x-dn$XlkgKB(MX&Xq7Z(d~&qjY#$J+4AT>G@(eDl zOpaJ8n_I@EoF-ITxe1`Rb@9j-5b5+L9-i1VOii0>oQk!Y5A@O@doX}oGS$6cp~2iB znnxN%@IB@3K_0{c0&KQW*#GDR6!K1Y6|w}p>$izqq+IU6^*!B>6MqXH4|>Lv1k-&n zsb8b!h)^fno%HnX+dVDJi*X|tu<l%oVw<1jHKysQe$DEMPZ=EM(r0rKIsILf8TD4y zBXE*2pzf{xET5W>lVT60MMa~Oe`(Wq^pTLJ4wW-lm;PGi*oGf$usE+A=QPMjA}dEb zTy-W_)72Xz3C%wxto7Drcl`Vcg2mD_V2@y9cq<1bUjq7$R}DuDsBX$r%k_j<e&l_I z-+jiGVj`|<!z9|V-};cp$3vBwFP#ec`Eh7xhl$Tomc9%a=DQd`jfiBY%;p|Gkl2>Z zDF+vtJlECYedIEinx#dR(7Nx{X&2i5hu<~=uYDY=!&pOrrNrV!f91<34#SM)08g8v z#PfI@X?7A<u&m*JCPfQJ>*5r9-}@|ibN|<}B<v2R&K_$n{QeGfiD!^e<8T_uXdmCV zLHb^+V)a_nnq3JVD`%_)L%JqD&DTXvVO>7fB&mlYJKiW71gVkC8U6H8E_g~iT<H58 zJ#`j(hc7x=GPyf?@*PL+UGMfv*N-<ZrP<BcLKm}nT9|lG=uGht#yd)yNDZiN?jNEi zpO+?PAsD>Ez3!*@cr25zyjf^NO*}YB;ybqs@zgCE>R5H=)>XvK@0fci*S#Y2&Iu(^ zWd*OklGA3VlCF4{e=V<1GIZ5+oqo~vmeg%_<Tdxq3AOw^`ve1M^gLkG%@R8BM0ji- zH9jpT?9^{3#PNWB<wkpGEq0oJ`%*|^-jZSGMph3Tq@fvZ`pMmI_)a<+pUI*0>$B~e zBkTUjEtL8BuyjWS8qHUYyh_n8!q%K#nK09&m}_<%5s`^0on89ZVvjptL`6YORQfJ; zuPo!wL)-@V<>!wALdg%yQpWUQ9+EE)itkSGmb$mRRX7e!yB;xt(47cpA(9AVb#aQt zN62(8=FchX65TF$RxMCa2q=FDi}H}j;L*5H4ss*`IWVl=j9|PP6)Jw3X_vvSju4nz z@ZazD_8D8DeKaS@O2}XX5#JO(Orna&ADR1l_Dt2mH<h_)4ITe#wY4YixDp;TJc>Va zj0m*1#bi4p8l<q?+&2<F|8ok{r{Hm>;+>kAaDfm?T%xcX%9qj__H9!YGY<%bTc>+| z%}*}hJzU!^nnO6_Qh7RXi9&Uk-^SgycX*7Am{;NepW$W@N)Jg)>jhkl@RsHEZEXuq z2Pvq>yPiBNevapUJ6A|!*E+xQ?}Eg?xZYC&8vtndIsfm11Q&qu_x*b}3;+QC9sKiF z2B80X{KS6?1_10{Sz3AWcse;A=+D6o2S{%zK#VtJxGBEj%uTkqH9APvWQ74Qi?h?y z*v4mEXj0sF*BNLW;du(FGqUle&&b?eZ4S;(?O>hWts;!=NR)>@HzWOajTp7BaC$?F z0pV-t(%{?2G)-z}_XB%hce-p9@x9z#xE7YSor$wg-{;@92=^R1YKP8PvWzT+&Fmd? z+4^dj47EPT)@99agZ%hlhfJ&;W{--|7%NUoo9u_^=%~-B<M9+ufR`WNQSSF)(U#A6 zLb@P(?eJ3DvON4EF)9^XW^9{!{@ZWF4&@DE*o-=LDK_O~EE?v$Uo5^Yo!dmZK6QAg zUtXeXHyI09kT@Lkq`9g0#1gOna9iFJHa}jdW^;dg;<1p{T2b6ek#ta|y?4Io&ti41 zR-w(`qA0{F+7A@y-N=pRq!<fg<It(FFt^?8O<F6pQh}%oPC)DDUldY0PRhIZvm1DY zk{b<GNN2C2Qx&DbQSmZXs+#cV8r}3v*NA-HQIv9N61TUV0oANK3EPUJ8GGqR*RBR@ z5n9IF`bJhbWu;DN4!%1@!-Q<O{&NkckdpdXIO_7*jCNnecAkEG)v#Qqc63y(HD~PX z7o{@$_5QR3H_3XgK5TM1T<<;iGwF4_gLM9(ei#i~c|S8w?O2I{N?c0WtTv2;l~tvM zETc>)Z;^Gc-j0;#l>2MB2s0&L5_yJE2D_7ae5tX(+KZ(ANhF4qF{uL``OPbtmmHIx z2z>I7Dg_X1a8FnyQU-5QM3K($Bg*6HkCfLIA(rpLYl#;l3yu1;<*U1HyQ<0d9CUS_ z4?)(J^Xdz>G3|@y?ay&J;b+t2GX@bZB}2<B$?XD5H<V<%GvJax?k~f(DWCT{xZ=s^ zEM^r$*3&peq>?}DhlyYes0Y3tQ{eGf%)sSWkQ)T?3{%prl`tIw-9MKw6TnpS!UtLE zGma}o^mM>_f*XTFHug0jdk9-YO)n*^R4)9>Oay#R6G#0+^PeB*W^ru9nj+%rD7=#D zHX|-)B)u>nZ3k4dgn9-Ni5BDyCL&i~7dw$4CpD)CwDa?fPjR^Q+mcpE`%$8o6>%?% z%|-3<b3D0v2YFa4(M7cpVlywb|G-h31P>NO1S7zYr&Dx>6=oIUea`~|Gopn4P_NiU zUuS-jsEXy~5D~@G(^g~a`4N;vx28*SPy~kHeM29LZUSor3kRUPNZDE91}F*p?6UHF z;TYZth>^W?z*%5Yt)8fD*f)aU5KTs`LLAhww66uSOqod!m8HM{8c?kgo;UBJP^FY7 zQ#bC+__B*k0L}?8y8cr<xUY-TrJzDaLBI}eE~^5E75E(#M*O)8Sm`t;Y#c;tT~JH- z&FsBUl+4~TE9FB72_L8ZCvX&J=2Rl>mm9hl%B<@Q#r6*JV}j+;2?>xIdFObWoKRT- zUCUQQ3P%j#4An_bOvF$R_<|KC6y7dmFYA=o-uC8zYRiIQQlvWXkP{dN1QOz!qspCP zK#Dkf+@o*sT8aRR)o{!AZTexl1k8PA<enf`GZ4^KIkK2R0!7l*&?8d%q7eLegO1N! zaP^~Qa_NcG_YM1Mh^!}^F5m)cMMsDKsZu_L*metEfx#C-3z+N|2|!1?MT0+>BuuG_ zjQkeoAfrmj4e357Fq#e%1B7ZAG)pxB<H-22Fc9rxN@o<?8ZlYpZkc^nAVV<Hb% zT#*Qj&B1`$EUF@=FjtEZCpv&3S`fi-L=-N=vy5|hJvkSwi+!x?vKe4}Ul$&fb^~!1 z(t>O_)m-&u4pw-Wucb(^++B~qM=qWY!UUOP&UVufViH257^KY6y`_9vwm>eqURYr3 zksRngNlkccgbAs1V+0vN1uYmOoLF9)T^Gbo6<~ypK+g=6wg-N|Ytzl7Y6CLSi=Z#Q z%n(+v$^bgr?7tyJqpXuj>^I34O)7S9r%bA_F|H!#!Ck|*-NXF>In%(Z4koA)0pKV- z@QyNqe<OQ{Qx8?s#MudMQvfLI4(HP37I)))^H%>P;-LW2hTq5nY(XGCrj5EBNIYPI z8ZA&W+F8*iG+(?Xl)^j)&NokpPongqTv{emkdK}3i<x_ksK2{Q!$L))e<n<~Rd7yx z(R@jN`Q=V?q*r~4QT(*xRu1-Tak)f_N|~dVVEG&B)Z3OMG5a#q_Dj)y`P;+;Tm5LB zH{Y)SV`2;{k7gTT7olTCIkj6rEk;_xv&k-dH?blm=WY5!fZE-qWTyK(tX?S-ZSF=k ziP2s#(<mA?Zr9qXuQ$Jx(jh5%aP!tfpZH?nz_e%q(H96kvR=uqx||M!)GZPk8c1y( zxS#lW%!^(>Ht%2d=qF0OzFemM=sfmjrLt3W$#<a>cD}+>MN^Nm=kM;u(s|Q!;;yzk zkc{<i12_Wb{K)9%bv`e%S~km34*pPvVp^D+Y&}q=B#sW|h4v8Dnx=bq9xhFn5+}r7 zM<p7(;TLafw{==QjQIX$!7n;8cg#{>*XxN%cEzKkip2|9V;O>QWZ%~_Bk#V6{YPxE zp=4_;DiEpc=;hhGWq;+~$n3|O?tPd}uyFAH)9_KF%2g!t1H415{Ro%#t>Bfb-=*cr z<v`EY)6wkqOSD~o5z7~P%73n@rLWpA)MZY+YD~@50%)o|(yP*X*2_6(T-Y-jJ*}_y zD4WR7sHY4f=v>A9XAEpMeR>ZKt(v&h@S!*FNa*Wr>U%GKM;;cp27?L@iO@<ymDy@= z=Gs1Oo-fJn?g(~VWhV={>4D&yMQk?HAb$j?H7zY`^4zh`yHP%&MFMC_fGy}S0qC6& zjX1cW7{ow7@|8;(ij<jv3^ESO!})D(G|U5P*oiT?u$$9VuXDqizPN9yK-pOq)y;NU zWg{!80PZSEkL7dBDUf$Og6eKjud0Y|m#3OK0-=KRd@fgKuZs0q!bsju_&j;ybK~jf z?tbFqce8*<ews-~&&V*&cyoTTb9`fY7qfmfvK2bcce(cU3-Nbswd{*`SI0rg-v_>b z@XyZnxnJu$9$Ke*dVk{RC+>f;+Srr5`Dolk{0<kNDBGujv7^m{zuH!~4cUHR_Yp8c z!@w1CcHXrB5`+4t^fp}hwWGS;G|w*fv~R{NAzD^S5D^8kN;THn7Ipm4zA&;<TCs~T zwqP!N1tpA7J~R}eH&G6QW9NLk>q^7@^F{Cao$XoEO<sSAw9cEO<HiSDHO(XI*9;7^ z{`;;|bU{`5jqiFA9#4Y4F}lOe5INOm-ZQ6%!}%;qtHTOw!Mfy5@fv7^$9mq2238gA zUyd*RLs!CAcNvKEE)<Rz>1ZCuTGe&gnl$0g!<vZ1=^r-dQlOViPrF5+%cl-vow^Tc zJIa!u)e{Nh#*(ey*Y!M_y*xUwY|1*sar9X|sMfE7w#L+5ZjM%9%=x7<EiB-|=?`>S z`gcZ@($isAJ9iBlot~dV+;)ua6TW6AY^LW$9}Q(ajawg|*zQ~$%a57FTawOtn%W|t z=^U^Af(Rb{xNz{5Ff2<_vZZdIAfsxSmFl48<hxH>u|KTnNbJBb@mkeDl4VBk^K{Bx zdQl5vc-O?JL&4JdON}tI@`^!jN3V#p{NRv8vCR}A%1sBlyYVbsc;=k3O(iT(iDwd+ zr5%fx17=>n|GY0D#iJn<RSDGFq{blZ_(+K}>!^a-`czoAt@JS)py$!=E_9uk*7W4v z$TFVMg>)jXHkPlA@g>>!AEToeIRRpJOI#5_&fP<GW<DCt>~V>1YjlH8Cc9!ajeJqL z)RXen%vS*vrq0$C2dEPlUcr_3ReGH^qj#9Uj562}ZO3DL03e<ASFMeP4g&n`eg5t$ z{_;Nm6Mq?Jzd51*Wcb~N`%i{XKgs@L_{)0JlK&^e?~TMSyY6pHqWy0SKh3*;Qv3?} ypLXI`$MH8baDSut)qngm^mlcB?)?qXKR^HKKGc;lv3{*#{v4t|)4KlmS^ovbc{x)6 literal 0 HcmV?d00001 diff --git a/F-Droid/test/assets/multiRepo.normal.jar b/F-Droid/test/assets/multiRepo.normal.jar new file mode 100644 index 0000000000000000000000000000000000000000..6a53256ebc4f7a670e7ee7af052814e3df29d1af GIT binary patch literal 5809 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 F{V&5kQ1$=- literal 0 HcmV?d00001 diff --git a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java new file mode 100644 index 000000000..d3efcb624 --- /dev/null +++ b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java @@ -0,0 +1,136 @@ + +package org.fdroid.fdroid; + +import android.content.Context; +import android.test.InstrumentationTestCase; + +import org.fdroid.fdroid.RepoUpdater.UpdateException; +import org.fdroid.fdroid.data.Repo; + +import java.io.File; +import java.util.UUID; + +public class MultiRepoUpdaterTest extends InstrumentationTestCase { + private static final String TAG = "RepoUpdaterTest"; + + private Context context; + private RepoUpdater conflictingRepoUpdater; + private RepoUpdater mainRepoUpdater; + private RepoUpdater archiveRepoUpdater; + private File testFilesDir; + + private static final String PUB_KEY = + "3082050b308202f3a003020102020420d8f212300d06092a864886f70d01010b050030363110300e0603" + + "55040b1307462d44726f69643122302006035504031319657073696c6f6e2e70657465722e7365727779" + + "6c6f2e636f6d301e170d3135303931323233313632315a170d3433303132383233313632315a30363110" + + "300e060355040b1307462d44726f69643122302006035504031319657073696c6f6e2e70657465722e73" + + "657277796c6f2e636f6d30820222300d06092a864886f70d01010105000382020f003082020a02820201" + + "00b21fe72b84ce721967851364bd20511088d117bc3034e4bb4d3c1a06af2a308fdffdaf63b12e0926b9" + + "0545134b9ff570646cbcad89d9e86dcc8eb9977dd394240c75bccf5e8ddc3c5ef91b4f16eca5f36c36f1" + + "92463ff2c9257d3053b7c9ecdd1661bd01ec3fe70ee34a7e6b92ddba04f258a32d0cfb1b0ce85d047180" + + "97fc4bdfb54541b430dfcfc1c84458f9eb5627e0ec5341d561c3f15f228379a1282d241329198f31a7ac" + + "cd51ab2bbb881a1da55001123483512f77275f8990c872601198065b4e0137ddd1482e4fdefc73b857d4" + + "be324ca96c268ceb725398f8cc38a0dc6aa2c277f8686724e8c7ff3f320a05791fccacc6caa956cf23a9" + + "de2dc7070b262c0e35d90d17e90773bb11e875e79a8dfd958e359d5d5ad903a7cbc2955102502bd0134c" + + "a1ff7a0bbbbb57302e4a251e40724dcaa8ad024f4b3a71b8fceaac664c0dcc1995a1c4cf42676edad8bc" + + "b03ba255ab796677f18fff2298e1aaa5b134254b44d08a4d934c9859af7bbaf078c37b7f628db0e2cffb" + + "0493a669d5f4770d35d71284550ce06d6f6811cd2a31585085716257a4ba08ad968b0a2bf88f34ca2f2c" + + "73af1c042ab147597faccfb6516ef4468cfa0c5ab3c8120eaa7bac1080e4d2310f717db20815d0e1ee26" + + "bd4e47eed8d790892017ae9595365992efa1b7fd1bc1963f018264b2b3749b8f7b1907bb0843f1e7fc2d" + + "3f3b02284cd4bae0ab0203010001a321301f301d0603551d0e0416041456110e4fed863ab1df9448bfd9" + + "e10a8bc32ffe08300d06092a864886f70d01010b050003820201008082572ae930ebc55ecf1110f4bb72" + + "ad2a952c8ac6e65bd933706beb4a310e23deabb8ef6a7e93eea8217ab1f3f57b1f477f95f1d62eccb563" + + "67a4d70dfa6fcd2aace2bb00b90af39412a9441a9fae2396ff8b93de1df3d9837c599b1f80b7d75285cb" + + "df4539d7dd9612f54b45ca59bc3041c9b92fac12753fac154d12f31df360079ab69a2d20db9f6a7277a8" + + "259035e93de95e8cbc80351bc83dd24256183ea5e3e1db2a51ea314cdbc120c064b77e2eb3a731530511" + + "1e1dabed6996eb339b7cb948d05c1a84d63094b4a4c6d11389b2a7b5f2d7ecc9a149dda6c33705ef2249" + + "58afdfa1d98cf646dcf8857cd8342b1e07d62cb4313f35ad209046a4a42ff73f38cc740b1e695eeda49d" + + "5ea0384ad32f9e3ae54f6a48a558dbc7cccabd4e2b2286dc9c804c840bd02b9937841a0e48db00be9e3c" + + "d7120cf0f8648ce4ed63923f0352a2a7b3b97fc55ba67a7a218b8c0b3cda4a45861280a622e0a59cc9fb" + + "ca1117568126c581afa4408b0f5c50293c212c406b8ab8f50aad5ed0f038cfca580ef3aba7df25464d9e" + + "495ffb629922cfb511d45e6294c045041132452f1ed0f20ac3ab4792f610de1734e4c8b71d743c4b0101" + + "98f848e0dbfce5a0f2da0198c47e6935a47fda12c518ef45adfb66ddf5aebaab13948a66c004b8592d22" + + "e8af60597c4ae2977977cf61dc715a572e241ae717cafdb4f71781943945ac52e0f50b"; + + @Override + protected void setUp() { + context = getInstrumentation().getContext(); + testFilesDir = TestUtils.getWriteableDir(getInstrumentation()); + conflictingRepoUpdater = createUpdater(context); + mainRepoUpdater = createUpdater(context); + archiveRepoUpdater = createUpdater(context); + } + + /** + * Check that a sample of expected apps and apk versions are available in the database. + * Also check that the AdAway apks versions 50-53 are as expected, given that 50 was in + * both conflicting and archive repo, and 51-53 were in both conflicting and main repo. + */ + private void assertExpected() { + + } + + public void testConflictingThenMainThenArchive() throws UpdateException { + if (updateConflicting() && updateMain() && updateArchive()) { + assertExpected(); + } + } + + public void testConflictingThenArchiveThenMain() throws UpdateException { + if (updateConflicting() && updateArchive() && updateMain()) { + assertExpected(); + } + } + + public void testArchiveThenMainThenConflicting() throws UpdateException { + if (updateArchive() && updateMain() && updateConflicting()) { + assertExpected(); + } + } + + public void testArchiveThenConflictingThenMain() throws UpdateException { + if (updateArchive() && updateConflicting() && updateMain()) { + assertExpected(); + } + } + + public void testMainThenArchiveThenConflicting() throws UpdateException { + if (updateMain() && updateArchive() && updateConflicting()) { + assertExpected(); + } + } + + public void testMainThenConflictingThenArchive() throws UpdateException { + if (updateMain() && updateConflicting() && updateArchive()) { + assertExpected(); + } + } + + private RepoUpdater createUpdater(Context context) { + Repo repo = new Repo(); + repo.pubkey = PUB_KEY; + return new RepoUpdater(context, repo); + } + + private boolean updateConflicting() throws UpdateException { + return updateRepo(conflictingRepoUpdater, "multiRepo.conflicting.jar"); + } + + private boolean updateMain() throws UpdateException { + return updateRepo(mainRepoUpdater, "multiRepo.normal.jar"); + } + + private boolean updateArchive() throws UpdateException { + return updateRepo(archiveRepoUpdater, "multiRepo.archive.jar"); + } + + private boolean updateRepo(RepoUpdater updater, String indexJarPath) throws UpdateException { + if (!testFilesDir.canWrite()) + return false; + + File indexJar = TestUtils.copyAssetToDir(context, indexJarPath, testFilesDir); + updater.processDownloadedFile(indexJar, UUID.randomUUID().toString()); + return true; + } + +} From 0685c16efef3e9b8d9035681a2084e7a30fe6cc6 Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Sat, 24 Oct 2015 09:00:27 +1100 Subject: [PATCH 4/7] More work on multi-repo tests, currently broken due to F-Droid being broken. (One of) the problems with F-Droid's multiple support is that there is a primary key on the fdroid_apk table which is a composite of: * id * vercode Which means that two repos providing the same version means one will update the other, rather than ending up with two different versions. Instead, there should be some other way to differentiate apks from different sources. Firstly, it should take into account the signing cert. Secondly, it may taken into account the hash, because two people could sign different apks with the same cert and then we are back at square one. --- .../org/fdroid/fdroid/data/RepoProvider.java | 4 + .../fdroid/FDroidTestWithAllProviders.java | 23 ++ .../fdroid/fdroid/MultiRepoUpdaterTest.java | 239 +++++++++++++++++- .../test/src/org/fdroid/fdroid/TestUtils.java | 76 +++--- 4 files changed, 299 insertions(+), 43 deletions(-) create mode 100644 F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java diff --git a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java index 5e27df4c2..62af6affd 100644 --- a/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java +++ b/F-Droid/src/org/fdroid/fdroid/data/RepoProvider.java @@ -244,6 +244,10 @@ public class RepoProvider extends FDroidProvider { matcher.addURI(AUTHORITY + "." + PROVIDER_NAME, "#", CODE_SINGLE); } + public static String getAuthority() { + return AUTHORITY + "." + PROVIDER_NAME; + } + public static Uri getContentUri() { return Uri.parse("content://" + AUTHORITY + "." + PROVIDER_NAME); } diff --git a/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java b/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java new file mode 100644 index 000000000..aad34777a --- /dev/null +++ b/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java @@ -0,0 +1,23 @@ +package org.fdroid.fdroid; + +import org.fdroid.fdroid.data.AppProvider; + +/** + * Class that makes available all ContentProviders that F-Droid owns. + */ +public abstract class FDroidTestWithAllProviders extends FDroidProviderTest<AppProvider> { + + public FDroidTestWithAllProviders(Class<AppProvider> providerClass, String providerAuthority) { + super(providerClass, providerAuthority); + } + + @Override + protected String[] getMinimalProjection() { + return new String[] { + AppProvider.DataColumns._ID, + AppProvider.DataColumns.APP_ID, + AppProvider.DataColumns.NAME, + }; + } + +} diff --git a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java index d3efcb624..0167a3031 100644 --- a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java +++ b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java @@ -1,23 +1,45 @@ package org.fdroid.fdroid; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.support.annotation.NonNull; import android.test.InstrumentationTestCase; +import android.test.RenamingDelegatingContext; +import android.test.mock.MockContentResolver; +import android.text.TextUtils; +import android.util.Log; import org.fdroid.fdroid.RepoUpdater.UpdateException; +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 java.io.File; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; public class MultiRepoUpdaterTest extends InstrumentationTestCase { private static final String TAG = "RepoUpdaterTest"; + private static final String REPO_MAIN = "Test F-Droid repo"; + private static final String REPO_ARCHIVE = "Test F-Droid repo (Archive)"; + private static final String REPO_CONFLICTING = "Test F-Droid repo with different apps"; + private Context context; private RepoUpdater conflictingRepoUpdater; private RepoUpdater mainRepoUpdater; private RepoUpdater archiveRepoUpdater; private File testFilesDir; + private RepoPersister persister; private static final String PUB_KEY = "3082050b308202f3a003020102020420d8f212300d06092a864886f70d01010b050030363110300e0603" + @@ -52,13 +74,75 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { "98f848e0dbfce5a0f2da0198c47e6935a47fda12c518ef45adfb66ddf5aebaab13948a66c004b8592d22" + "e8af60597c4ae2977977cf61dc715a572e241ae717cafdb4f71781943945ac52e0f50b"; + public class TestContext extends RenamingDelegatingContext { + + private MockContentResolver resolver; + + public TestContext() { + super(getInstrumentation().getTargetContext(), "test."); + + resolver = new MockContentResolver(); + resolver.addProvider(AppProvider.getAuthority(), prepareProvider(new AppProvider())); + resolver.addProvider(ApkProvider.getAuthority(), prepareProvider(new ApkProvider())); + resolver.addProvider(RepoProvider.getAuthority(), prepareProvider(new RepoProvider())); + } + + private ContentProvider prepareProvider(ContentProvider provider) { + provider.attachInfo(this, null); + provider.onCreate(); + return provider; + } + + @Override + public File getFilesDir() { + return getInstrumentation().getTargetContext().getFilesDir(); + } + + /** + * String resources used during testing (e.g. when bootstraping the database) are from + * the real org.fdroid.fdroid app, not the test org.fdroid.fdroid.test app. + */ + @Override + public Resources getResources() { + return getInstrumentation().getTargetContext().getResources(); + } + + @Override + public ContentResolver getContentResolver() { + return resolver; + } + + @Override + public AssetManager getAssets() { + return getInstrumentation().getContext().getAssets(); + } + + @Override + public File getDatabasePath(String name) { + return new File(getInstrumentation().getContext().getFilesDir(), "fdroid_test.db"); + } + } + @Override - protected void setUp() { - context = getInstrumentation().getContext(); + public void setUp() throws Exception { + super.setUp(); + + context = new TestContext(); + testFilesDir = TestUtils.getWriteableDir(getInstrumentation()); - conflictingRepoUpdater = createUpdater(context); - mainRepoUpdater = createUpdater(context); - archiveRepoUpdater = createUpdater(context); + + // On a fresh database install, there will be F-Droid + GP repos, including their Archive + // repos that we are not interested in. + RepoProvider.Helper.remove(context, 1); + RepoProvider.Helper.remove(context, 2); + RepoProvider.Helper.remove(context, 3); + RepoProvider.Helper.remove(context, 4); + + persister = new RepoPersister(context); + + conflictingRepoUpdater = createUpdater(REPO_CONFLICTING, context); + mainRepoUpdater = createUpdater(REPO_MAIN, context); + archiveRepoUpdater = createUpdater(REPO_ARCHIVE, context); } /** @@ -67,49 +151,189 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { * both conflicting and archive repo, and 51-53 were in both conflicting and main repo. */ private void assertExpected() { + Log.d(TAG, "Asserting all versions of each .apk are in index."); + persister.save(new ArrayList<Repo>(0)); + + List<Repo> repos = RepoProvider.Helper.all(context); + assertEquals("Repos", 3, repos.size()); + + assertMainRepo(repos); + assertMainArchiveRepo(repos); + assertConflictingRepo(repos); + + String appId = "com.uberspot.a2048"; + App app = AppProvider.Helper.findById(context.getContentResolver(), appId); + assertNotNull("App " + appId + " exists", app); + } + + /** + * + 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<Repo> allRepos) { + Repo repo = findRepo(REPO_MAIN, allRepos); + + List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.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<Repo> allRepos) { + Repo repo = findRepo(REPO_ARCHIVE, allRepos); + + List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.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<Repo> allRepos) { + Repo repo = findRepo(REPO_CONFLICTING, allRepos); + + List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.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 }); + } + + @NonNull + private Repo findRepo(@NonNull String name, List<Repo> allRepos) { + Repo repo = null; + for (Repo r : allRepos) { + if (TextUtils.equals(name, r.getName())) { + repo = r; + break; + } + } + + assertNotNull("Repo " + allRepos, repo); + return repo; + } + + /** + * Checks that each version of appId as specified in versionCodes is present in apksToCheck. + */ + private void assertApksExist(List<Apk> apksToCheck, String appId, int[] versionCodes) { + for (int versionCode : versionCodes) { + boolean found = false; + for (Apk apk : apksToCheck) { + if (apk.vercode == versionCode && apk.id.equals(appId)) { + found = true; + break; + } + } + + assertTrue("Found app " + appId + ", v" + versionCode, found); + } + } + + private void assertEmpty() { + assertEquals("No apps present", 0, AppProvider.Helper.all(context.getContentResolver()).size()); + + String[] packages = { + "com.uberspot.a2048", + "org.adaway", + "siir.es.adbWireless", + }; + + for (String id : packages) { + assertEquals("No apks for " + id, 0, ApkProvider.Helper.findByApp(context, id).size()); + } } public void testConflictingThenMainThenArchive() throws UpdateException { + assertEmpty(); if (updateConflicting() && updateMain() && updateArchive()) { assertExpected(); } } public void testConflictingThenArchiveThenMain() throws UpdateException { + assertEmpty(); if (updateConflicting() && updateArchive() && updateMain()) { assertExpected(); } } public void testArchiveThenMainThenConflicting() throws UpdateException { + assertEmpty(); if (updateArchive() && updateMain() && updateConflicting()) { assertExpected(); } } public void testArchiveThenConflictingThenMain() throws UpdateException { + assertEmpty(); if (updateArchive() && updateConflicting() && updateMain()) { assertExpected(); } } public void testMainThenArchiveThenConflicting() throws UpdateException { + assertEmpty(); if (updateMain() && updateArchive() && updateConflicting()) { assertExpected(); } } public void testMainThenConflictingThenArchive() throws UpdateException { + assertEmpty(); if (updateMain() && updateConflicting() && updateArchive()) { assertExpected(); } } - private RepoUpdater createUpdater(Context context) { + private RepoUpdater createUpdater(String name, Context context) { Repo repo = new Repo(); repo.pubkey = PUB_KEY; - return new RepoUpdater(context, repo); + repo.address = UUID.randomUUID().toString(); + repo.name = name; + + ContentValues values = new ContentValues(2); + values.put(RepoProvider.DataColumns.PUBLIC_KEY, repo.pubkey); + values.put(RepoProvider.DataColumns.ADDRESS, repo.address); + values.put(RepoProvider.DataColumns.NAME, repo.name); + + RepoProvider.Helper.insert(context, values); + + // Need to reload the repo based on address so that it includes the primary key from + // the database. + return new RepoUpdater(context, RepoProvider.Helper.findByAddress(context, repo.address)); } private boolean updateConflicting() throws UpdateException { @@ -130,6 +354,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { File indexJar = TestUtils.copyAssetToDir(context, indexJarPath, testFilesDir); updater.processDownloadedFile(indexJar, UUID.randomUUID().toString()); + persister.queueUpdater(updater); return true; } diff --git a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java index 027c36188..21b673bda 100644 --- a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java +++ b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java @@ -7,6 +7,7 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Environment; +import android.support.annotation.Nullable; import android.util.Log; import junit.framework.AssertionFailedError; @@ -192,6 +193,7 @@ public class TestUtils { } + @Nullable public static File copyAssetToDir(Context context, String assetName, File directory) { File tempFile; InputStream input = null; @@ -199,7 +201,7 @@ public class TestUtils { try { tempFile = File.createTempFile(assetName + "-", ".testasset", directory); Log.d(TAG, "Copying asset file " + assetName + " to directory " + directory); - input = context.getResources().getAssets().open(assetName); + input = context.getAssets().open(assetName); output = new FileOutputStream(tempFile); Utils.copy(input, output); } catch (IOException e) { @@ -212,6 +214,18 @@ public class TestUtils { return tempFile; } + public static File getWriteableDir(Context context) { + File[] dirsToTry = new File[] { + context.getCacheDir(), + context.getFilesDir(), + context.getExternalCacheDir(), + context.getExternalFilesDir(null), + Environment.getExternalStorageDirectory() + }; + + return getWriteableDir(dirsToTry); + } + /** * Prefer internal over external storage, because external tends to be FAT filesystems, * which don't support symlinks (which we test using this method). @@ -219,41 +233,31 @@ public class TestUtils { public static File getWriteableDir(Instrumentation instrumentation) { Context context = instrumentation.getContext(); Context targetContext = instrumentation.getTargetContext(); - File dir = context.getCacheDir(); - Log.d(TAG, "Looking for writeable dir, trying context.getCacheDir()"); - if (dir == null || !dir.canWrite()) { - Log.d(TAG, "Looking for writeable dir, trying context.getFilesDir()"); - dir = context.getFilesDir(); + + + File[] dirsToTry = new File[] { + context.getCacheDir(), + context.getFilesDir(), + targetContext.getCacheDir(), + targetContext.getFilesDir(), + context.getExternalCacheDir(), + context.getExternalFilesDir(null), + targetContext.getExternalCacheDir(), + targetContext.getExternalFilesDir(null), + Environment.getExternalStorageDirectory() + }; + + return getWriteableDir(dirsToTry); + } + + private static File getWriteableDir(File[] dirsToTry) { + + for (File dir : dirsToTry) { + if (dir != null && dir.canWrite()) { + return dir; + } } - if (dir == null || !dir.canWrite()) { - Log.d(TAG, "Looking for writeable dir, trying targetContext.getCacheDir()"); - dir = targetContext.getCacheDir(); - } - if (dir == null || !dir.canWrite()) { - Log.d(TAG, "Looking for writeable dir, trying targetContext.getFilesDir()"); - dir = targetContext.getFilesDir(); - } - if (dir == null || !dir.canWrite()) { - Log.d(TAG, "Looking for writeable dir, trying context.getExternalCacheDir()"); - dir = context.getExternalCacheDir(); - } - if (dir == null || !dir.canWrite()) { - Log.d(TAG, "Looking for writeable dir, trying context.getExternalFilesDir(null)"); - dir = context.getExternalFilesDir(null); - } - if (dir == null || !dir.canWrite()) { - Log.d(TAG, "Looking for writeable dir, trying targetContext.getExternalCacheDir()"); - dir = targetContext.getExternalCacheDir(); - } - if (dir == null || !dir.canWrite()) { - Log.d(TAG, "Looking for writeable dir, trying targetContext.getExternalFilesDir(null)"); - dir = targetContext.getExternalFilesDir(null); - } - if (dir == null || !dir.canWrite()) { - Log.d(TAG, "Looking for writeable dir, trying Environment.getExternalStorageDirectory()"); - dir = Environment.getExternalStorageDirectory(); - } - Log.d(TAG, "Writeable dir found: " + dir); - return dir; + + return null; } } From 1c179848b7510cb11e3582c427624d0390223db1 Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Wed, 4 Nov 2015 21:39:10 +1100 Subject: [PATCH 5/7] Added tests for current multi-repo behaviour. This is the bare minimum of what must be maintained going forward. Ideally the behaviour sohould be better, but that is for the future. --- .../fdroid/fdroid/MultiRepoUpdaterTest.java | 120 +++++++++++++++--- 1 file changed, 103 insertions(+), 17 deletions(-) diff --git a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java index 0167a3031..9a02c5818 100644 --- a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java +++ b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java @@ -146,25 +146,53 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { } /** - * Check that a sample of expected apps and apk versions are available in the database. - * Also check that the AdAway apks versions 50-53 are as expected, given that 50 was in - * both conflicting and archive repo, and 51-53 were in both conflicting and main repo. + * 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.d(TAG, "Asserting all versions of each .apk are in index."); - - persister.save(new ArrayList<Repo>(0)); - List<Repo> repos = RepoProvider.Helper.all(context); assertEquals("Repos", 3, repos.size()); assertMainRepo(repos); assertMainArchiveRepo(repos); assertConflictingRepo(repos); + } - String appId = "com.uberspot.a2048"; - App app = AppProvider.Helper.findById(context.getContentResolver(), appId); - assertNotNull("App " + appId + " exists", app); + /** + * + */ + private void assertSomewhatAcceptable() { + Log.d(TAG, "Asserting at least one versions of each .apk is in index."); + List<Repo> repos = RepoProvider.Helper.all(context); + assertEquals("Repos", 3, repos.size()); + + assertApp2048(); + assertAppAdaway(); + assertAppAdbWireless(); + assertAppIcsImport(); + } + + private void assertApp(String packageName, int[] versionCodes) { + List<Apk> apks = ApkProvider.Helper.findByApp(context, packageName, ApkProvider.DataColumns.ALL); + assertApksExist(apks, packageName, versionCodes); + } + + private void assertApp2048() { + assertApp("com.uberspot.a2048", new int[]{ 19, 18 }); + } + + private void assertAppAdaway() { + assertApp("org.adaway", new int[]{ 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 42, 40, 38, 37, 36, 35 }); + } + + private void assertAppAdbWireless() { + assertApp("siir.es.adbWireless", new int[]{ 12 }); + } + + private void assertAppIcsImport() { + assertApp("org.dgtale.icsimport", new int[] { 3, 2 }); } /** @@ -184,7 +212,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.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, "org.adaway", new int[]{52, 53, 54}); assertApksExist(apks, "siir.es.adbWireless", new int[]{12}); } @@ -228,7 +256,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.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 }); + assertApksExist(apks, "org.dgtale.icsimport", new int[]{2, 3}); } @NonNull @@ -276,48 +304,106 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { } } - public void testConflictingThenMainThenArchive() throws UpdateException { + private void persistData() { + persister.save(new ArrayList<Repo>(0)); + } + + public void testCorrectConflictingThenMainThenArchive() throws UpdateException { assertEmpty(); if (updateConflicting() && updateMain() && updateArchive()) { + persistData(); assertExpected(); } } - public void testConflictingThenArchiveThenMain() throws UpdateException { + public void testCorrectConflictingThenArchiveThenMain() throws UpdateException { assertEmpty(); if (updateConflicting() && updateArchive() && updateMain()) { + persistData(); assertExpected(); } } - public void testArchiveThenMainThenConflicting() throws UpdateException { + public void testCorrectArchiveThenMainThenConflicting() throws UpdateException { assertEmpty(); if (updateArchive() && updateMain() && updateConflicting()) { + persistData(); assertExpected(); } } - public void testArchiveThenConflictingThenMain() throws UpdateException { + public void testCorrectArchiveThenConflictingThenMain() throws UpdateException { assertEmpty(); if (updateArchive() && updateConflicting() && updateMain()) { + persistData(); assertExpected(); } } - public void testMainThenArchiveThenConflicting() throws UpdateException { + public void testCorrectMainThenArchiveThenConflicting() throws UpdateException { assertEmpty(); if (updateMain() && updateArchive() && updateConflicting()) { + persistData(); assertExpected(); } } - public void testMainThenConflictingThenArchive() throws UpdateException { + public void testCorrectMainThenConflictingThenArchive() throws UpdateException { assertEmpty(); if (updateMain() && updateConflicting() && updateArchive()) { + persistData(); assertExpected(); } } + public void testAcceptableConflictingThenMainThenArchive() throws UpdateException { + assertEmpty(); + if (updateConflicting() && updateMain() && updateArchive()) { + persistData(); + assertSomewhatAcceptable(); + } + } + + public void testAcceptableConflictingThenArchiveThenMain() throws UpdateException { + assertEmpty(); + if (updateConflicting() && updateArchive() && updateMain()) { + persistData(); + assertSomewhatAcceptable(); + } + } + + public void testAcceptableArchiveThenMainThenConflicting() throws UpdateException { + assertEmpty(); + if (updateArchive() && updateMain() && updateConflicting()) { + persistData(); + assertSomewhatAcceptable(); + } + } + + public void testAcceptableArchiveThenConflictingThenMain() throws UpdateException { + assertEmpty(); + if (updateArchive() && updateConflicting() && updateMain()) { + persistData(); + assertSomewhatAcceptable(); + } + } + + public void testAcceptableMainThenArchiveThenConflicting() throws UpdateException { + assertEmpty(); + if (updateMain() && updateArchive() && updateConflicting()) { + persistData(); + assertSomewhatAcceptable(); + } + } + + public void testAcceptableMainThenConflictingThenArchive() throws UpdateException { + assertEmpty(); + if (updateMain() && updateConflicting() && updateArchive()) { + persistData(); + assertSomewhatAcceptable(); + } + } + private RepoUpdater createUpdater(String name, Context context) { Repo repo = new Repo(); repo.pubkey = PUB_KEY; From 938c992023bb95ae1cae6e0677717e78e1a84a45 Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Wed, 4 Nov 2015 21:43:34 +1100 Subject: [PATCH 6/7] Comment out tests for future, desirable behaviour. Leave only the ones which align with the current multi-repo behaviour that F-Droid exhibits. The commented out tests can be uncommented in the future when working on proper multi-repo support. --- .../test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java index 9a02c5818..19c187d68 100644 --- a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java +++ b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java @@ -308,6 +308,10 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { persister.save(new ArrayList<Repo>(0)); } + /* 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 + ucommented and all these tests should pass: + public void testCorrectConflictingThenMainThenArchive() throws UpdateException { assertEmpty(); if (updateConflicting() && updateMain() && updateArchive()) { @@ -356,6 +360,8 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { } } + */ + public void testAcceptableConflictingThenMainThenArchive() throws UpdateException { assertEmpty(); if (updateConflicting() && updateMain() && updateArchive()) { From 12d5c5c7b49bba5bf884f5a6c5d909104c3b5b80 Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Sat, 7 Nov 2015 09:40:44 +1100 Subject: [PATCH 7/7] Format to make checkstyle happy. Remove unused code. --- .../src/org/fdroid/fdroid/RepoPersister.java | 24 +++--- .../fdroid/FDroidTestWithAllProviders.java | 23 ------ .../fdroid/fdroid/MultiRepoUpdaterTest.java | 81 +++++++++---------- .../test/src/org/fdroid/fdroid/TestUtils.java | 58 ++++++------- 4 files changed, 75 insertions(+), 111 deletions(-) delete mode 100644 F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java diff --git a/F-Droid/src/org/fdroid/fdroid/RepoPersister.java b/F-Droid/src/org/fdroid/fdroid/RepoPersister.java index 3f82422be..57aba7d34 100644 --- a/F-Droid/src/org/fdroid/fdroid/RepoPersister.java +++ b/F-Droid/src/org/fdroid/fdroid/RepoPersister.java @@ -34,13 +34,13 @@ public class RepoPersister { * values changed in the index, some fields should not be updated. Rather, they should be * ignored, because they were explicitly set by the user, and hence can't be automatically * overridden by the index. - * + * <p/> * NOTE: In the future, these attributes will be moved to a join table, so that the app table * is essentially completely transient, and can be nuked at any time. */ private static final String[] APP_FIELDS_TO_IGNORE = { - AppProvider.DataColumns.IGNORE_ALLUPDATES, - AppProvider.DataColumns.IGNORE_THISUPDATE, + AppProvider.DataColumns.IGNORE_ALLUPDATES, + AppProvider.DataColumns.IGNORE_THISUPDATE, }; @NonNull @@ -129,9 +129,9 @@ public class RepoPersister { List<Apk> toRemove = new ArrayList<>(); final String[] fields = { - ApkProvider.DataColumns.APK_ID, - ApkProvider.DataColumns.VERSION_CODE, - ApkProvider.DataColumns.VERSION, + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.VERSION_CODE, + ApkProvider.DataColumns.VERSION, }; for (final Repo repo : updatedRepos) { @@ -175,14 +175,14 @@ public class RepoPersister { List<ContentProviderOperation> operations, int currentCount, int totalUpdateCount) - throws RemoteException, OperationApplicationException { + throws RemoteException, OperationApplicationException { int i = 0; while (i < operations.size()) { int count = Math.min(operations.size() - i, 100); ArrayList<ContentProviderOperation> o = new ArrayList<>(operations.subList(i, i + count)); UpdateService.sendStatus(context, UpdateService.STATUS_INFO, context.getString( - R.string.status_inserting, - (int) ((double) (currentCount + i) / totalUpdateCount * 100))); + R.string.status_inserting, + (int) ((double) (currentCount + i) / totalUpdateCount * 100))); context.getContentResolver().applyBatch(providerAuthority, o); i += 100; } @@ -193,9 +193,9 @@ public class RepoPersister { */ private List<Apk> getKnownApks(List<Apk> apks) { final String[] fields = { - ApkProvider.DataColumns.APK_ID, - ApkProvider.DataColumns.VERSION, - ApkProvider.DataColumns.VERSION_CODE, + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.VERSION, + ApkProvider.DataColumns.VERSION_CODE, }; return ApkProvider.Helper.knownApks(context, apks, fields); } diff --git a/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java b/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java deleted file mode 100644 index aad34777a..000000000 --- a/F-Droid/test/src/org/fdroid/fdroid/FDroidTestWithAllProviders.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.fdroid.fdroid; - -import org.fdroid.fdroid.data.AppProvider; - -/** - * Class that makes available all ContentProviders that F-Droid owns. - */ -public abstract class FDroidTestWithAllProviders extends FDroidProviderTest<AppProvider> { - - public FDroidTestWithAllProviders(Class<AppProvider> providerClass, String providerAuthority) { - super(providerClass, providerAuthority); - } - - @Override - protected String[] getMinimalProjection() { - return new String[] { - AppProvider.DataColumns._ID, - AppProvider.DataColumns.APP_ID, - AppProvider.DataColumns.NAME, - }; - } - -} diff --git a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java index 19c187d68..71c6e43ff 100644 --- a/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java +++ b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java @@ -17,7 +17,6 @@ import android.util.Log; import org.fdroid.fdroid.RepoUpdater.UpdateException; 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; @@ -42,7 +41,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { private RepoPersister persister; private static final String PUB_KEY = - "3082050b308202f3a003020102020420d8f212300d06092a864886f70d01010b050030363110300e0603" + + "3082050b308202f3a003020102020420d8f212300d06092a864886f70d01010b050030363110300e0603" + "55040b1307462d44726f69643122302006035504031319657073696c6f6e2e70657465722e7365727779" + "6c6f2e636f6d301e170d3135303931323233313632315a170d3433303132383233313632315a30363110" + "300e060355040b1307462d44726f69643122302006035504031319657073696c6f6e2e70657465722e73" + @@ -180,31 +179,31 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { } private void assertApp2048() { - assertApp("com.uberspot.a2048", new int[]{ 19, 18 }); + assertApp("com.uberspot.a2048", new int[]{19, 18}); } private void assertAppAdaway() { - assertApp("org.adaway", new int[]{ 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 42, 40, 38, 37, 36, 35 }); + assertApp("org.adaway", new int[]{54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 42, 40, 38, 37, 36, 35}); } private void assertAppAdbWireless() { - assertApp("siir.es.adbWireless", new int[]{ 12 }); + assertApp("siir.es.adbWireless", new int[]{12}); } private void assertAppIcsImport() { - assertApp("org.dgtale.icsimport", new int[] { 3, 2 }); + assertApp("org.dgtale.icsimport", new int[]{3, 2}); } /** - * + 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) + * + 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<Repo> allRepos) { Repo repo = findRepo(REPO_MAIN, allRepos); @@ -217,38 +216,38 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { } /** - * + 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) + * + 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<Repo> allRepos) { Repo repo = findRepo(REPO_ARCHIVE, allRepos); List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.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 }); + 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) * + * - 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) + * - Version 1.2 (3) + * - Version 1.1 (2) */ private void assertConflictingRepo(List<Repo> allRepos) { Repo repo = findRepo(REPO_CONFLICTING, allRepos); @@ -294,9 +293,9 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { assertEquals("No apps present", 0, AppProvider.Helper.all(context.getContentResolver()).size()); String[] packages = { - "com.uberspot.a2048", - "org.adaway", - "siir.es.adbWireless", + "com.uberspot.a2048", + "org.adaway", + "siir.es.adbWireless", }; for (String id : packages) { @@ -308,7 +307,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { persister.save(new ArrayList<Repo>(0)); } - /* At time fo writing, the following tests did not pass. This is because the multi-repo support + /* 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 ucommented and all these tests should pass: @@ -360,7 +359,7 @@ public class MultiRepoUpdaterTest extends InstrumentationTestCase { } } - */ + */ public void testAcceptableConflictingThenMainThenArchive() throws UpdateException { assertEmpty(); diff --git a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java index 21b673bda..0869383de 100644 --- a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java +++ b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java @@ -68,10 +68,10 @@ public class TestUtils { if (actualList.size() != expectedContains.size()) { String message = "List sizes don't match.\n" + - "Expected: " + - listToString(expectedContains) + "\n" + - "Actual: " + - listToString(actualList); + "Expected: " + + listToString(expectedContains) + "\n" + + "Actual: " + + listToString(actualList); throw new AssertionFailedError(message); } for (T required : expectedContains) { @@ -85,10 +85,10 @@ public class TestUtils { if (!containsRequired) { String message = "List doesn't contain \"" + required + "\".\n" + - "Expected: " + - listToString(expectedContains) + "\n" + - "Actual: " + - listToString(actualList); + "Expected: " + + listToString(expectedContains) + "\n" + + "Actual: " + + listToString(actualList); throw new AssertionFailedError(message); } } @@ -151,8 +151,8 @@ public class TestUtils { * "installed apps" table in the database. */ public static void installAndBroadcast( - MockContextSwappableComponents context, MockInstallablePackageManager pm, - String appId, int versionCode, String versionName) { + MockContextSwappableComponents context, MockInstallablePackageManager pm, + String appId, int versionCode, String versionName) { context.setPackageManager(pm); pm.install(appId, versionCode, versionName); @@ -166,8 +166,8 @@ public class TestUtils { * @see org.fdroid.fdroid.TestUtils#installAndBroadcast(mock.MockContextSwappableComponents, mock.MockInstallablePackageManager, String, int, String) */ public static void upgradeAndBroadcast( - MockContextSwappableComponents context, MockInstallablePackageManager pm, - String appId, int versionCode, String versionName) { + MockContextSwappableComponents context, MockInstallablePackageManager pm, + String appId, int versionCode, String versionName) { /* removeAndBroadcast(context, pm, appId); installAndBroadcast(context, pm, appId, versionCode, versionName); @@ -214,18 +214,6 @@ public class TestUtils { return tempFile; } - public static File getWriteableDir(Context context) { - File[] dirsToTry = new File[] { - context.getCacheDir(), - context.getFilesDir(), - context.getExternalCacheDir(), - context.getExternalFilesDir(null), - Environment.getExternalStorageDirectory() - }; - - return getWriteableDir(dirsToTry); - } - /** * Prefer internal over external storage, because external tends to be FAT filesystems, * which don't support symlinks (which we test using this method). @@ -235,21 +223,21 @@ public class TestUtils { Context targetContext = instrumentation.getTargetContext(); - File[] dirsToTry = new File[] { - context.getCacheDir(), - context.getFilesDir(), - targetContext.getCacheDir(), - targetContext.getFilesDir(), - context.getExternalCacheDir(), - context.getExternalFilesDir(null), - targetContext.getExternalCacheDir(), - targetContext.getExternalFilesDir(null), - Environment.getExternalStorageDirectory() + File[] dirsToTry = new File[]{ + context.getCacheDir(), + context.getFilesDir(), + targetContext.getCacheDir(), + targetContext.getFilesDir(), + context.getExternalCacheDir(), + context.getExternalFilesDir(null), + targetContext.getExternalCacheDir(), + targetContext.getExternalFilesDir(null), + Environment.getExternalStorageDirectory(), }; return getWriteableDir(dirsToTry); } - + private static File getWriteableDir(File[] dirsToTry) { for (File dir : dirsToTry) {