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..57aba7d34 --- /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 appsToUpdate = new HashMap<>(); + private List apksToUpdate = new ArrayList<>(); + private List 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 apps) { + for (final App app : apps) { + appsToUpdate.put(app.id, app); + } + } + + private void queueApks(List apks) { + apksToUpdate.addAll(apks); + } + + public void save(List disabledRepos) { + + List 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 apks) { + final CompatibilityChecker checker = new CompatibilityChecker(context); + for (final Apk apk : apks) { + final List 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 apksToUpdate, List updatedRepos) { + + long startTime = System.currentTimeMillis(); + List toRemove = new ArrayList<>(); + + final String[] fields = { + ApkProvider.DataColumns.APK_ID, + ApkProvider.DataColumns.VERSION_CODE, + ApkProvider.DataColumns.VERSION, + }; + + for (final Repo repo : updatedRepos) { + final List 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 appsToUpdate, int totalUpdateCount, int currentCount) { + + List operations = new ArrayList<>(); + List 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 operations, + int currentCount, + int totalUpdateCount) + throws RemoteException, OperationApplicationException { + int i = 0; + while (i < operations.size()) { + int count = Math.min(operations.size() - i, 100); + ArrayList 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 getKnownApks(List 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 apksToUpdate, int totalApksAppsCount, int currentCount) { + + List operations = new ArrayList<>(); + + List 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 apksToUpdate) { + for (final Apk apkToUpdate : apksToUpdate) { + if (apkToUpdate.vercode == existingApk.vercode && apkToUpdate.id.equals(existingApk.id)) { + return true; + } + } + return false; + } + + private void removeApksFromRepos(List 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 getKnownAppIds(List apps) { + List knownAppIds = new ArrayList<>(); + if (apps.isEmpty()) { + return knownAppIds; + } + if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) { + int middle = apps.size() / 2; + List apps1 = apps.subList(0, middle); + List 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 getKnownAppIdsFromProvider(List 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 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 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 repos = RepoProvider.Helper.all(this); // Process each repo... - Map appsToUpdate = new HashMap<>(); - List apksToUpdate = new ArrayList<>(); + RepoPersister appSaver = new RepoPersister(this); + //List swapRepos = new ArrayList<>(); List unchangedRepos = new ArrayList<>(); List 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 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 apks) { - final CompatibilityChecker checker = new CompatibilityChecker(context); - for (final Apk apk : apks) { - final List 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 getKnownAppIds(List apps) { - List knownAppIds = new ArrayList<>(); - if (apps.isEmpty()) { - return knownAppIds; - } - if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) { - int middle = apps.size() / 2; - List apps1 = apps.subList(0, middle); - List 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 getKnownAppIdsFromProvider(List 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 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 appsToUpdate, int totalUpdateCount, int currentCount) { - - List operations = new ArrayList<>(); - List 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 operations, - int currentCount, - int totalUpdateCount) - throws RemoteException, OperationApplicationException { - int i = 0; - while (i < operations.size()) { - int count = Math.min(operations.size() - i, 100); - ArrayList 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 getKnownApks(List 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 apksToUpdate, int totalApksAppsCount, int currentCount) { - - List operations = new ArrayList<>(); - - List 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 apksToUpdate, List updatedRepos) { - - long startTime = System.currentTimeMillis(); - List toRemove = new ArrayList<>(); - - final String[] fields = { - ApkProvider.DataColumns.APK_ID, - ApkProvider.DataColumns.VERSION_CODE, - ApkProvider.DataColumns.VERSION, - }; - - for (final Repo repo : updatedRepos) { - final List 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 apksToUpdate) { - for (final Apk apkToUpdate : apksToUpdate) { - if (apkToUpdate.vercode == existingApk.vercode && apkToUpdate.id.equals(existingApk.id)) { - return true; - } - } - return false; - } - - private void removeApksFromRepos(List 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); } } 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/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 000000000..f5c505c07 Binary files /dev/null and b/F-Droid/test/assets/multiRepo.archive.jar differ diff --git a/F-Droid/test/assets/multiRepo.conflicting.jar b/F-Droid/test/assets/multiRepo.conflicting.jar new file mode 100644 index 000000000..6d26f65d3 Binary files /dev/null and b/F-Droid/test/assets/multiRepo.conflicting.jar differ diff --git a/F-Droid/test/assets/multiRepo.normal.jar b/F-Droid/test/assets/multiRepo.normal.jar new file mode 100644 index 000000000..6a53256eb Binary files /dev/null and b/F-Droid/test/assets/multiRepo.normal.jar differ 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..71c6e43ff --- /dev/null +++ b/F-Droid/test/src/org/fdroid/fdroid/MultiRepoUpdaterTest.java @@ -0,0 +1,452 @@ + +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.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" + + "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"; + + 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 + public void setUp() throws Exception { + super.setUp(); + + context = new TestContext(); + + testFilesDir = TestUtils.getWriteableDir(getInstrumentation()); + + // 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); + } + + /** + * 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."); + List repos = RepoProvider.Helper.all(context); + assertEquals("Repos", 3, repos.size()); + + assertMainRepo(repos); + assertMainArchiveRepo(repos); + assertConflictingRepo(repos); + } + + /** + * + */ + private void assertSomewhatAcceptable() { + Log.d(TAG, "Asserting at least one versions of each .apk is in index."); + List repos = RepoProvider.Helper.all(context); + assertEquals("Repos", 3, repos.size()); + + assertApp2048(); + assertAppAdaway(); + assertAppAdbWireless(); + assertAppIcsImport(); + } + + private void assertApp(String packageName, int[] versionCodes) { + List 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}); + } + + /** + * + 2048 (com.uberspot.a2048) + * - Version 1.96 (19) + * - Version 1.95 (18) + * + AdAway (org.adaway) + * - Version 3.0.2 (54) + * - Version 3.0.1 (53) + * - Version 3.0 (52) + * + adbWireless (siir.es.adbWireless) + * - Version 1.5.4 (12) + */ + private void assertMainRepo(List allRepos) { + Repo repo = findRepo(REPO_MAIN, allRepos); + + List apks = ApkProvider.Helper.findByRepo(context, repo, 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 allRepos) { + Repo repo = findRepo(REPO_ARCHIVE, allRepos); + + List 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 allRepos) { + Repo repo = findRepo(REPO_CONFLICTING, allRepos); + + List 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 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 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()); + } + } + + private void persistData() { + persister.save(new ArrayList(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()) { + persistData(); + assertExpected(); + } + } + + public void testCorrectConflictingThenArchiveThenMain() throws UpdateException { + assertEmpty(); + if (updateConflicting() && updateArchive() && updateMain()) { + persistData(); + assertExpected(); + } + } + + public void testCorrectArchiveThenMainThenConflicting() throws UpdateException { + assertEmpty(); + if (updateArchive() && updateMain() && updateConflicting()) { + persistData(); + assertExpected(); + } + } + + public void testCorrectArchiveThenConflictingThenMain() throws UpdateException { + assertEmpty(); + if (updateArchive() && updateConflicting() && updateMain()) { + persistData(); + assertExpected(); + } + } + + public void testCorrectMainThenArchiveThenConflicting() throws UpdateException { + assertEmpty(); + if (updateMain() && updateArchive() && updateConflicting()) { + persistData(); + assertExpected(); + } + } + + 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; + 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 { + 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()); + persister.queueUpdater(updater); + return true; + } + +} 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"; diff --git a/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java b/F-Droid/test/src/org/fdroid/fdroid/TestUtils.java index 027c36188..0869383de 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; @@ -67,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) { @@ -84,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); } } @@ -150,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); @@ -165,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); @@ -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) { @@ -219,41 +221,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; } }