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;
}
}